From 83743d5af02035c8046668d84f1bd7c2106d157c Mon Sep 17 00:00:00 2001 From: FFXN <31929997+FFXN@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:43:22 +0800 Subject: [PATCH] feat: evaluation (#35419) Co-authored-by: jyong <718720800@qq.com> Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hj24 Co-authored-by: hj24 Co-authored-by: Joel Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: CodingOnStar Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/workflows/style.yml | 14 +- .gitignore | 3 - .vite-hooks/pre-commit | 45 +- api/controllers/console/__init__.py | 8 +- api/controllers/console/app/app.py | 31 +- api/controllers/console/app/audio.py | 2 +- api/controllers/console/app/completion.py | 2 +- api/controllers/console/app/generator.py | 2 +- api/controllers/console/app/mcp_server.py | 32 +- api/controllers/console/app/workflow.py | 15 +- .../console/app/workflow_app_log.py | 2 +- api/controllers/console/app/workflow_run.py | 4 +- api/controllers/console/auth/oauth_server.py | 2 +- api/controllers/console/datasets/datasets.py | 2 +- .../console/datasets/datasets_document.py | 170 +--- .../console/datasets/datasets_segments.py | 2 +- .../console/datasets/hit_testing_base.py | 2 +- .../datasets/rag_pipeline/datasource_auth.py | 4 +- .../rag_pipeline_draft_variable.py | 2 +- .../rag_pipeline/rag_pipeline_workflow.py | 2 +- .../console/evaluation/evaluation.py | 2 + api/controllers/console/explore/audio.py | 2 +- api/controllers/console/explore/completion.py | 2 +- api/controllers/console/explore/message.py | 2 +- api/controllers/console/explore/trial.py | 4 +- api/controllers/console/explore/workflow.py | 4 +- api/controllers/console/remote_files.py | 2 +- .../console/workspace/agent_providers.py | 2 +- api/controllers/console/workspace/endpoint.py | 2 +- .../workspace/load_balancing_config.py | 4 +- .../console/workspace/model_providers.py | 6 +- api/controllers/console/workspace/models.py | 6 +- api/controllers/console/workspace/plugin.py | 2 +- .../console/workspace/tool_providers.py | 2 +- .../console/workspace/trigger_providers.py | 2 +- api/controllers/inner_api/plugin/plugin.py | 2 +- api/controllers/inner_api/plugin/wraps.py | 16 +- api/controllers/mcp/mcp.py | 2 +- api/controllers/service_api/app/audio.py | 2 +- api/controllers/service_api/app/completion.py | 2 +- .../service_api/app/conversation.py | 2 +- .../service_api/dataset/dataset.py | 2 +- .../service_api/dataset/segment.py | 2 +- .../service_api/workspace/models.py | 2 +- api/controllers/web/audio.py | 2 +- api/controllers/web/completion.py | 2 +- api/controllers/web/message.py | 2 +- api/controllers/web/remote_files.py | 2 +- api/controllers/web/workflow.py | 4 +- api/core/agent/cot_agent_runner.py | 17 +- api/core/agent/cot_chat_agent_runner.py | 3 +- api/core/agent/cot_completion_agent_runner.py | 3 +- api/core/agent/fc_agent_runner.py | 15 +- .../agent/output_parser/cot_output_parser.py | 3 +- .../easy_ui_based_app/model_config/manager.py | 3 +- .../prompt_template/manager.py | 3 +- api/core/app/app_config/entities.py | 8 +- .../features/file_upload/manager.py | 3 +- .../variables/manager.py | 3 +- .../app/apps/advanced_chat/app_generator.py | 9 +- api/core/app/apps/advanced_chat/app_runner.py | 12 +- api/core/app/apps/agent_chat/app_generator.py | 2 +- .../base_app_generate_response_converter.py | 3 +- api/core/app/apps/base_app_queue_manager.py | 2 +- api/core/app/apps/base_app_runner.py | 21 +- api/core/app/apps/chat/app_generator.py | 2 +- api/core/app/apps/chat/app_runner.py | 4 +- .../common/graph_runtime_state_support.py | 3 +- .../common/workflow_response_converter.py | 26 +- api/core/app/apps/completion/app_generator.py | 2 +- api/core/app/apps/completion/app_runner.py | 4 +- .../app/apps/pipeline/pipeline_generator.py | 4 +- api/core/app/apps/pipeline/pipeline_runner.py | 12 +- api/core/app/apps/workflow/app_generator.py | 10 +- api/core/app/apps/workflow/app_runner.py | 11 +- .../apps/workflow/generate_task_pipeline.py | 6 +- api/core/app/apps/workflow_app_runner.py | 66 +- api/core/app/entities/app_invoke_entities.py | 4 +- api/core/app/entities/queue_entities.py | 8 +- api/core/app/entities/task_entities.py | 8 +- .../hosting_moderation/hosting_moderation.py | 3 +- .../app/layers/pause_state_persist_layer.py | 4 +- api/core/app/layers/timeslice_layer.py | 2 +- api/core/app/layers/trigger_post_layer.py | 4 +- api/core/app/llm/model_access.py | 9 +- api/core/app/llm/quota.py | 2 +- .../based_generate_task_pipeline.py | 2 +- api/core/app/workflow/layers/llm_quota.py | 9 +- api/core/app/workflow/layers/persistence.py | 15 +- .../base/tts/app_generator_tts_publisher.py | 5 +- api/core/datasource/datasource_manager.py | 6 +- api/core/datasource/entities/api_entities.py | 2 +- .../datasource/utils/message_transformer.py | 3 +- api/core/entities/execution_extra_content.py | 2 +- api/core/entities/mcp_provider.py | 2 +- api/core/entities/model_entities.py | 3 +- api/core/entities/provider_entities.py | 2 +- .../helper/code_executor/code_executor.py | 2 +- api/core/hosting_configuration.py | 2 +- .../output_parser/structured_output.py | 10 +- api/core/mcp/server/streamable_http.py | 3 +- api/core/mcp/utils.py | 2 +- api/core/memory/token_buffer_memory.py | 18 +- api/core/plugin/backwards_invocation/model.py | 27 +- api/core/plugin/backwards_invocation/node.py | 3 +- api/core/plugin/entities/plugin.py | 2 +- api/core/plugin/entities/plugin_daemon.py | 4 +- api/core/plugin/entities/request.py | 8 +- api/core/plugin/impl/base.py | 16 +- api/core/plugin/impl/model.py | 13 +- api/core/plugin/utils/converter.py | 3 +- api/core/prompt/advanced_prompt_transform.py | 15 +- .../prompt/agent_history_prompt_transform.py | 11 +- api/core/prompt/prompt_transform.py | 5 +- api/core/prompt/simple_prompt_transform.py | 13 +- api/core/prompt/utils/prompt_message_util.py | 3 +- api/core/provider_manager.py | 16 +- api/core/rag/datasource/retrieval_service.py | 34 +- api/core/rag/datasource/vdb/vector_factory.py | 2 +- api/core/rag/docstore/dataset_docstore.py | 2 +- .../processor/paragraph_index_processor.py | 20 +- api/core/rag/models/document.py | 3 +- api/core/rag/rerank/rerank_model.py | 5 +- api/core/rag/rerank/weight_rerank.py | 2 +- .../multi_dataset_function_call_router.py | 5 +- .../celery_workflow_execution_repository.py | 2 +- ...lery_workflow_node_execution_repository.py | 2 +- api/core/repositories/factory.py | 2 +- ...qlalchemy_workflow_execution_repository.py | 6 +- ...hemy_workflow_node_execution_repository.py | 8 +- .../builtin_tool/providers/audio/tools/asr.py | 7 +- .../builtin_tool/providers/audio/tools/tts.py | 3 +- api/core/tools/builtin_tool/tool.py | 5 +- api/core/tools/custom_tool/tool.py | 2 +- api/core/tools/entities/api_entities.py | 2 +- api/core/tools/mcp_tool/tool.py | 3 +- api/core/tools/tool_engine.py | 2 +- api/core/tools/tool_file_manager.py | 2 +- api/core/tools/tool_manager.py | 5 +- .../dataset_multi_retriever_tool.py | 2 +- api/core/tools/utils/message_transformer.py | 2 +- .../tools/utils/model_invocation_utils.py | 7 +- .../utils/workflow_configuration_sync.py | 5 +- api/core/tools/workflow_as_tool/provider.py | 2 +- api/core/tools/workflow_as_tool/tool.py | 4 +- api/core/trigger/debug/event_selectors.py | 2 +- api/core/workflow/nodes/agent/entities.py | 4 +- .../workflow/nodes/datasource/entities.py | 5 +- .../nodes/knowledge_index/entities.py | 4 +- .../nodes/knowledge_retrieval/entities.py | 6 +- .../knowledge_retrieval_node.py | 11 +- .../workflow/nodes/trigger_plugin/entities.py | 4 +- .../trigger_plugin/trigger_event_node.py | 5 +- .../nodes/trigger_schedule/entities.py | 4 +- .../trigger_schedule/trigger_schedule_node.py | 5 +- .../nodes/trigger_webhook/entities.py | 6 +- .../workflow/nodes/trigger_webhook/node.py | 9 +- .../event_handlers/create_document_index.py | 31 +- .../create_site_record_when_app_created.py | 8 +- ...rameters_cache_when_sync_draft_workflow.py | 5 +- ...oin_when_app_published_workflow_updated.py | 2 +- api/extensions/ext_sentry.py | 3 +- ..._api_workflow_node_execution_repository.py | 2 +- .../logstore_api_workflow_run_repository.py | 2 +- .../logstore_workflow_execution_repository.py | 4 +- api/extensions/otel/parser/base.py | 10 +- api/extensions/otel/parser/llm.py | 4 +- api/extensions/otel/parser/retrieval.py | 6 +- api/extensions/otel/parser/tool.py | 8 +- api/factories/variable_factory.py | 11 +- api/fields/member_fields.py | 2 +- api/fields/message_fields.py | 2 +- api/fields/raws.py | 1 - api/fields/workflow_fields.py | 3 +- api/libs/helper.py | 4 +- api/models/__init__.py | 2 + api/models/dataset.py | 43 +- api/models/evaluation.py | 2 +- api/models/model.py | 6 +- api/models/provider.py | 2 +- api/models/workflow.py | 77 +- .../src/dify_trace_aliyun/aliyun_trace.py | 4 +- .../src/dify_trace_aliyun/utils.py | 4 +- .../arize_phoenix_trace.py | 2 +- .../dify_trace_langsmith/langsmith_trace.py | 2 +- .../src/dify_trace_mlflow/mlflow_trace.py | 2 +- .../src/dify_trace_opik/opik_trace.py | 2 +- .../src/dify_trace_tencent/tencent_trace.py | 8 +- .../src/dify_trace_weave/weave_trace.py | 2 +- .../api_workflow_run_repository.py | 4 +- ..._api_workflow_node_execution_repository.py | 2 +- .../sqlalchemy_api_workflow_run_repository.py | 6 +- ...hemy_execution_extra_content_repository.py | 6 +- api/services/app_dsl_service.py | 12 +- api/services/app_task_service.py | 3 +- api/services/audio_service.py | 2 +- .../clear_free_plan_tenant_expired_logs.py | 2 +- api/services/conversation_service.py | 2 +- api/services/conversation_variable_updater.py | 2 +- api/services/datasource_provider_service.py | 2 +- .../entities/model_provider_entities.py | 18 +- api/services/file_service.py | 2 +- .../human_input_delivery_test_service.py | 2 +- api/services/human_input_service.py | 12 +- api/services/model_load_balancing_service.py | 12 +- api/services/rag_pipeline/rag_pipeline.py | 18 +- .../rag_pipeline/rag_pipeline_dsl_service.py | 12 +- .../archive_paid_plan_workflow_run.py | 2 +- api/services/snippet_service.py | 22 +- .../tools/api_tools_manage_service.py | 353 +++----- api/services/trigger/trigger_service.py | 2 +- api/services/variable_truncator.py | 3 +- api/services/vector_service.py | 2 +- api/services/workflow/workflow_converter.py | 13 +- .../workflow_draft_variable_service.py | 26 +- .../workflow_event_snapshot_service.py | 8 +- api/services/workflow_restore.py | 2 + api/services/workflow_service.py | 35 +- .../app_generate/workflow_execute_task.py | 2 +- .../batch_create_segment_to_index_task.py | 2 +- api/tasks/human_input_timeout_tasks.py | 4 +- api/tasks/mail_human_input_delivery_task.py | 2 +- api/tasks/workflow_execution_tasks.py | 4 +- api/tasks/workflow_node_execution_tasks.py | 6 +- .../test_datasource_manager_integration.py | 3 +- .../model_runtime/__mock/plugin_model.py | 6 +- .../test_workflow_draft_variable_service.py | 8 +- .../test_remove_app_and_related_data_task.py | 4 +- .../workflow/nodes/__mock/model.py | 3 +- .../workflow/nodes/test_code.py | 10 +- .../workflow/nodes/test_llm.py | 11 +- .../workflow/nodes/test_template_transform.py | 7 +- ...test_chat_conversation_status_count_api.py | 2 +- .../layers/test_pause_state_persist_layer.py | 14 +- .../test_human_input_form_repository_impl.py | 2 +- .../test_human_input_resume_node_execution.py | 22 +- .../factories/test_storage_key_loader.py | 2 +- ..._api_workflow_node_execution_repository.py | 2 +- ..._sqlalchemy_api_workflow_run_repository.py | 8 +- .../services/test_agent_service.py | 1 + .../services/test_app_dsl_service.py | 2 +- .../services/test_dataset_service.py | 2 +- .../test_dataset_service_update_dataset.py | 2 +- .../test_delete_archived_workflow_run.py | 2 +- .../test_human_input_delivery_test.py | 4 +- .../services/test_model_provider_service.py | 8 +- .../services/test_workflow_app_service.py | 2 +- .../test_workflow_draft_variable_service.py | 2 +- ...kflow_node_execution_service_repository.py | 2 +- .../tasks/test_clean_notion_document_task.py | 174 +++- .../test_mail_human_input_delivery_task.py | 6 +- .../test_remove_app_and_related_data_task.py | 4 +- .../test_workflow_pause_integration.py | 4 +- .../trigger/test_trigger_e2e.py | 2 +- .../app/test_workflow_pause_details_api.py | 8 +- .../app/workflow_draft_variables_test.py | 2 +- .../workspace/test_load_balancing_config.py | 3 +- .../controllers/service_api/app/test_audio.py | 2 +- .../service_api/app/test_completion.py | 2 +- .../service_api/app/test_workflow.py | 2 +- .../service_api/app/test_workflow_fields.py | 3 +- .../features/file_upload/test_manager.py | 3 +- .../test_app_runner_conversation_variables.py | 2 +- .../chat/test_base_app_runner_multimodal.py | 4 +- .../test_graph_runtime_state_support.py | 2 +- .../test_workflow_response_converter.py | 3 +- ...workflow_response_converter_human_input.py | 5 +- ..._workflow_response_converter_resumption.py | 5 +- ..._workflow_response_converter_truncation.py | 4 +- .../core/app/apps/test_base_app_generator.py | 5 +- .../test_workflow_app_runner_notifications.py | 4 +- .../test_workflow_app_runner_single_node.py | 4 +- .../app/apps/test_workflow_pause_events.py | 10 +- .../workflow/test_generate_task_pipeline.py | 5 +- ...est_conversation_variable_persist_layer.py | 7 +- .../layers/test_pause_state_persist_layer.py | 20 +- ...st_easy_ui_based_generate_task_pipeline.py | 4 +- .../datasource/test_datasource_manager.py | 6 +- .../core/mcp/server/test_streamable_http.py | 2 +- .../core/plugin/test_plugin_runtime.py | 16 +- .../core/plugin/utils/test_chunk_merger.py | 2 +- .../prompt/test_advanced_prompt_transform.py | 14 +- .../core/prompt/test_prompt_message.py | 5 +- .../rag/embedding/test_embedding_service.py | 8 +- .../core/rag/indexing/test_indexing_runner.py | 2 +- ...st_celery_workflow_execution_repository.py | 4 +- ...lery_workflow_node_execution_repository.py | 6 +- .../test_human_input_form_repository_impl.py | 10 +- ...rkflow_node_execution_conflict_handling.py | 10 +- ...test_workflow_node_execution_truncation.py | 10 +- api/tests/unit_tests/core/test_file.py | 1 + .../unit_tests/core/test_model_manager.py | 2 +- .../core/test_provider_configuration.py | 18 +- .../utils/test_workflow_configuration_sync.py | 2 +- .../core/tools/workflow_as_tool/test_tool.py | 2 +- .../core/variables/test_segment_type.py | 1 - .../variables/test_segment_type_validation.py | 1 - .../core/variables/test_variables.py | 3 +- .../workflow/graph_engine/layers/conftest.py | 6 +- .../graph_engine/layers/test_observability.py | 2 +- .../workflow/graph_engine/test_mock_nodes.py | 9 +- .../test_parallel_human_input_join_resume.py | 15 +- .../graph_engine/test_table_runner.py | 11 +- .../workflow/nodes/base/test_base_node.py | 4 +- .../test_get_node_type_classes_mapping.py | 3 +- .../workflow/nodes/code/code_node_spec.py | 3 +- .../test_http_request_executor.py | 8 +- .../test_human_input_form_filled_event.py | 7 +- .../test_knowledge_retrieval_node.py | 8 +- .../workflow/nodes/list_operator/node_spec.py | 4 +- .../core/workflow/nodes/llm/test_node.py | 26 +- .../test_parameter_extractor_node.py | 4 +- .../core/workflow/nodes/test_base_node.py | 8 +- .../nodes/test_document_extractor_node.py | 4 +- .../core/workflow/nodes/test_if_else.py | 10 +- .../core/workflow/nodes/test_list_operator.py | 4 +- .../nodes/test_start_node_json_object.py | 8 +- .../workflow/nodes/webhook/test_exceptions.py | 2 +- .../core/workflow/test_variable_pool.py | 18 +- .../core/workflow/test_workflow_entry.py | 12 +- .../test_workflow_entry_redis_channel.py | 5 +- .../factories/test_build_from_mapping.py | 2 +- .../factories/test_variable_factory.py | 10 +- .../libs/_human_input/test_form_service.py | 2 +- .../libs/_human_input/test_models.py | 2 +- .../models/test_conversation_variable.py | 3 +- api/tests/unit_tests/models/test_model.py | 2 +- api/tests/unit_tests/models/test_workflow.py | 7 +- .../unit_tests/models/test_workflow_models.py | 2 +- .../services/document_service_validation.py | 2 +- .../services/test_conversation_service.py | 795 ++++++++++++++++++ .../services/test_human_input_service.py | 12 +- ...est_model_provider_service_sanitization.py | 4 +- .../services/test_variable_truncator.py | 2 +- .../test_workflow_run_service_pause.py | 2 +- .../workflow/test_draft_var_loader_simple.py | 6 +- .../test_workflow_draft_variable_service.py | 8 +- .../test_workflow_event_snapshot_service.py | 6 +- .../tasks/test_human_input_timeout_tasks.py | 2 +- api/tests/unit_tests/tools/test_mcp_tool.py | 2 +- .../test_structured_output_parser.py | 6 +- api/uv.lock | 18 +- sdks/nodejs-client/tsconfig.json | 12 +- web/app/components/app-sidebar/basic.tsx | 8 +- .../app-sidebar/dataset-info/menu-item.tsx | 2 +- .../app-publisher/__tests__/index.spec.tsx | 174 ++++ .../components/app/app-publisher/index.tsx | 2 +- .../config-prompt/advanced-prompt-input.tsx | 4 +- .../config-prompt/simple-prompt-input.tsx | 4 +- .../app/configuration/config-var/index.tsx | 6 +- .../base/audio-gallery/AudioPlayer.tsx | 4 +- .../__tests__/chat-wrapper.spec.tsx | 126 +-- .../header/__tests__/index.spec.tsx | 29 +- .../base/chat/chat-with-history/hooks.tsx | 2 +- .../base/chat/chat/citation/popup.tsx | 18 +- web/app/components/base/chat/utils.ts | 12 +- web/app/components/base/copy-icon/index.tsx | 4 +- .../base/corner-label/index.stories.tsx | 2 +- .../annotation-reply/config-param.tsx | 4 +- .../__tests__/index.spec.tsx | 31 +- .../moderation/moderation-setting-modal.tsx | 4 +- .../mixed-variable-text-input/placeholder.tsx | 4 +- .../base/form/hooks/use-check-validated.ts | 2 +- .../base/grid-mask/__tests__/index.spec.tsx | 32 +- .../components/base/image-uploader/hooks.ts | 12 +- .../image-uploader/image-list.stories.tsx | 4 +- .../base/input/__tests__/index.spec.tsx | 48 +- .../components/base/logo/index.stories.tsx | 8 +- .../__tests__/code-block.spec.tsx | 58 +- .../markdown-blocks/__tests__/form.spec.tsx | 33 +- .../base/markdown/__tests__/index.spec.tsx | 8 +- .../base/modal-like-wrap/index.stories.tsx | 4 +- web/app/components/base/notion-icon/index.tsx | 2 +- .../page-selector/__tests__/index.spec.tsx | 90 +- .../component-picker-block/prompt-option.tsx | 2 +- .../__tests__/component.spec.tsx | 14 +- .../__tests__/pre-populate.spec.tsx | 6 +- .../plugins/query-block/component.tsx | 2 +- .../__tests__/component.spec.tsx | 6 +- .../plugins/workflow-variable-block/node.tsx | 18 +- .../base/prompt-log-modal/index.tsx | 4 +- .../components/base/select/locale-signin.tsx | 2 +- web/app/components/base/spinner/index.tsx | 2 +- .../components/base/tag-management/index.tsx | 4 +- .../components/base/tag-management/panel.tsx | 10 +- .../cloud-plan-item/__tests__/index.spec.tsx | 57 +- .../pricing/plans/cloud-plan-item/index.tsx | 16 +- .../datasets/common/image-previewer/index.tsx | 22 +- .../datasets/create/step-one/upgrade-card.tsx | 4 +- .../create/step-two/__tests__/index.spec.tsx | 189 ++--- .../components/__tests__/inputs.spec.tsx | 27 +- .../firecrawl/__tests__/options.spec.tsx | 63 +- .../watercrawl/__tests__/options.spec.tsx | 44 +- .../components/__tests__/operations.spec.tsx | 33 +- .../document-list/__tests__/index.spec.tsx | 81 +- .../__tests__/document-table-row.spec.tsx | 51 +- .../website-crawl/base/options/index.tsx | 2 +- .../process-documents/form.tsx | 2 +- .../query-input/__tests__/index.spec.tsx | 59 +- .../__tests__/modal.spec.tsx | 72 +- .../__tests__/index.spec.tsx | 136 +-- .../index-method/__tests__/index.spec.tsx | 29 +- .../settings/index-method/keyword-number.tsx | 10 +- .../settings/permission-selector/index.tsx | 12 +- .../header/account-setting/index.tsx | 2 +- .../key-validator/KeyInput.tsx | 4 +- .../account-setting/members-page/index.tsx | 28 +- .../model-auth/credential-selector.tsx | 8 +- .../status-indicators.tsx | 4 +- .../model-load-balancing-modal.tsx | 2 +- web/app/components/header/index.tsx | 6 +- .../plugins/marketplace/description/index.tsx | 18 +- .../search-box/__tests__/index.spec.tsx | 187 ++-- .../__tests__/detail-header.spec.tsx | 104 ++- .../app-selector/__tests__/index.spec.tsx | 340 ++++---- .../app-selector/app-picker.tsx | 8 +- .../app-selector/index.tsx | 2 +- .../__tests__/tts-params-panel.spec.tsx | 172 +--- .../__tests__/index.spec.tsx | 16 +- .../components/reasoning-config-form.tsx | 4 +- .../plugin-item/__tests__/action.spec.tsx | 112 +-- .../plugin-tasks/__tests__/index.spec.tsx | 143 ++-- .../__tests__/index.spec.tsx | 133 +-- .../panel/input-field/editor/form/index.tsx | 2 +- .../document-processing/options.tsx | 4 +- .../components/tools/marketplace/index.tsx | 28 +- .../mcp/detail/__tests__/content.spec.tsx | 73 +- .../tools/provider/__tests__/detail.spec.tsx | 84 +- .../__tests__/method-selector.spec.tsx | 60 +- .../start-node-option.tsx | 4 +- .../workflow/header/running-title.tsx | 2 +- .../workflow/hooks/use-checklist.ts | 88 +- .../_base/components/editor/text-editor.tsx | 2 +- .../mixed-variable-text-input/placeholder.tsx | 4 +- .../components/title-description-input.tsx | 6 +- .../components/variable/constant-field.tsx | 2 +- .../components/variable/output-var-list.tsx | 12 +- .../_base/components/variable/var-list.tsx | 26 +- .../nodes/_base/hooks/use-output-var-list.ts | 14 +- .../components/workflow/nodes/http/node.tsx | 4 +- .../workflow/nodes/iteration-start/index.tsx | 2 +- .../condition-list/condition-number.tsx | 4 +- .../condition-list/condition-string.tsx | 4 +- .../workflow/nodes/loop-start/index.tsx | 2 +- .../components/extract-parameter/item.tsx | 8 +- .../workflow/nodes/trigger-webhook/panel.tsx | 15 +- .../components/object-value-item.tsx | 6 +- .../run/utils/format-log/iteration/index.ts | 2 +- web/eslint.config.mjs | 24 +- 449 files changed, 3817 insertions(+), 3370 deletions(-) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 29f5b090f8..c32fc9d0cb 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -77,8 +77,6 @@ jobs: with: files: | web/** - e2e/** - sdks/nodejs-client/** packages/** package.json pnpm-lock.yaml @@ -97,14 +95,14 @@ jobs: id: eslint-cache-restore uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - path: .eslintcache - key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }} + path: web/.eslintcache + key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}- + ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}- - name: Web style check if: steps.changed-files.outputs.any_changed == 'true' - working-directory: . + working-directory: ./web run: vp run lint:ci - name: Web tsslint @@ -114,7 +112,7 @@ jobs: - name: Web type check if: steps.changed-files.outputs.any_changed == 'true' - working-directory: . + working-directory: ./web run: vp run type-check - name: Web dead code check @@ -126,7 +124,7 @@ jobs: if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true' uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - path: .eslintcache + path: web/.eslintcache key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }} superlinter: diff --git a/.gitignore b/.gitignore index 3493a7c756..53dea88899 100644 --- a/.gitignore +++ b/.gitignore @@ -203,7 +203,6 @@ sdks/python-client/dify_client.egg-info .vscode/* !.vscode/launch.json.template -!.vscode/settings.example.json !.vscode/README.md api/.vscode # vscode Code History Extension @@ -243,5 +242,3 @@ scripts/stress-test/reports/ # Code Agent Folder .qoder/* - -.eslintcache diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit index d48381bce2..cced022568 100755 --- a/.vite-hooks/pre-commit +++ b/.vite-hooks/pre-commit @@ -56,9 +56,44 @@ if $api_modified; then fi fi -if $skip_web_checks; then - echo "Git operation in progress, skipping web checks" - exit 0 -fi +if $web_modified; then + if $skip_web_checks; then + echo "Git operation in progress, skipping web checks" + exit 0 + fi -vp staged + echo "Running ESLint on web module" + + if git diff --cached --quiet -- 'web/**/*.ts' 'web/**/*.tsx'; then + web_ts_modified=false + else + ts_diff_status=$? + if [ $ts_diff_status -eq 1 ]; then + web_ts_modified=true + else + echo "Unable to determine staged TypeScript changes (git exit code: $ts_diff_status)." + exit $ts_diff_status + fi + fi + + cd ./web || exit 1 + pnpm exec vp staged + + if $web_ts_modified; then + echo "Running TypeScript type-check:tsgo" + if ! npm run type-check:tsgo; then + echo "Type check failed. Please run 'npm run type-check:tsgo' to fix the errors." + exit 1 + fi + else + echo "No staged TypeScript changes detected, skipping type-check:tsgo" + fi + + echo "Running knip" + if ! npm run knip; then + echo "Knip check failed. Please run 'npm run knip' to fix the errors." + exit 1 + fi + + cd ../ +fi diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index 7302a4edf5..23351beed9 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -126,6 +126,8 @@ from .snippets import snippet_workflow, snippet_workflow_draft_variable from .socketio import workflow as socketio_workflow # pyright: ignore[reportUnusedImport] # Import snippet controllers +from .snippets import snippet_workflow, snippet_workflow_draft_variable + # Import tag controllers from .tag import tags @@ -213,12 +215,12 @@ __all__ = [ "setup", "site", "snippet_workflow", - "snippet_workflow", "snippet_workflow_draft_variable", - "snippet_workflow_draft_variable", - "snippets", "snippets", "socketio_workflow", + "snippet_workflow", + "snippet_workflow_draft_variable", + "snippets", "spec", "statistic", "tags", diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 2ac4aef311..75a18a477a 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -5,6 +5,7 @@ from typing import Any, Literal from flask import request from flask_restx import Resource +from graphon.enums import WorkflowExecutionStatus from pydantic import AliasChoices, BaseModel, Field, computed_field, field_validator from sqlalchemy import select from sqlalchemy.orm import Session @@ -29,11 +30,11 @@ from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.trigger.constants import TRIGGER_NODE_TYPES from extensions.ext_database import db from fields.base import ResponseModel -from graphon.enums import WorkflowExecutionStatus from libs.helper import build_icon_url from libs.login import current_account_with_tenant, login_required from models import App, DatasetPermissionEnum, Workflow from models.model import IconType +from models.workflow import resolve_workflow_kind from services.app_dsl_service import AppDslService from services.app_service import AppService from services.enterprise.enterprise_service import EnterpriseService @@ -330,6 +331,7 @@ class AppPartial(ResponseModel): author_name: str | None = None has_draft_trigger: bool | None = None workflow_type: str | None = None + workflow_kind: str | None = None @computed_field(return_type=str | None) # type: ignore @property @@ -365,6 +367,7 @@ class AppDetail(ResponseModel): updated_at: int | None = None access_mode: str | None = None workflow_type: str | None = None + workflow_kind: str | None = None tags: list[Tag] = Field(default_factory=list) @field_validator("created_at", "updated_at", mode="before") @@ -508,15 +511,23 @@ class AppListApi(Resource): app.has_draft_trigger = str(app.id) in draft_trigger_app_ids workflow_ids = [str(app.workflow_id) for app in app_pagination.items if app.workflow_id] - workflow_type_map: dict[str, str] = {} + workflow_info_map: dict[str, tuple[str, str]] = {} if workflow_ids: rows = db.session.execute( - select(Workflow.id, Workflow.type).where(Workflow.id.in_(workflow_ids)) + select(Workflow.id, Workflow.type, Workflow.kind).where(Workflow.id.in_(workflow_ids)) ).all() - workflow_type_map = {str(row.id): row.type for row in rows} + workflow_info_map = { + str(row.id): ( + row.type.value if hasattr(row.type, "value") else str(row.type), + resolve_workflow_kind(row.kind).value, + ) + for row in rows + } for app in app_pagination.items: - app.workflow_type = workflow_type_map.get(str(app.workflow_id)) if app.workflow_id else None + workflow_info = workflow_info_map.get(str(app.workflow_id)) if app.workflow_id else None + app.workflow_type = workflow_info[0] if workflow_info else None + app.workflow_kind = workflow_info[1] if workflow_info else None pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True) return pagination_model.model_dump(mode="json"), 200 @@ -566,11 +577,15 @@ class AppApi(Resource): if app_model.workflow_id: row = db.session.execute( - select(Workflow.type).where(Workflow.id == app_model.workflow_id) - ).scalar() - app_model.workflow_type = row or None + select(Workflow.type, Workflow.kind).where(Workflow.id == app_model.workflow_id) + ).first() + app_model.workflow_type = ( + (row.type.value if hasattr(row.type, "value") else str(row.type)) if row else None + ) + app_model.workflow_kind = resolve_workflow_kind(row.kind).value if row else None else: app_model.workflow_type = None + app_model.workflow_kind = None response_model = AppDetailWithSite.model_validate(app_model, from_attributes=True) return response_model.model_dump(mode="json") diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index 91fbe4a85a..78ddb904e1 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -2,6 +2,7 @@ import logging from flask import request from flask_restx import Resource, fields +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field from werkzeug.exceptions import InternalServerError @@ -22,7 +23,6 @@ from controllers.console.app.error import ( from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError from libs.login import login_required from models import App, AppMode from services.audio_service import AudioService diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index fe274e4c9a..d83925d173 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -3,6 +3,7 @@ from typing import Any, Literal from flask import request from flask_restx import Resource +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound @@ -26,7 +27,6 @@ from core.errors.error import ( QuotaExceededError, ) from core.helper.trace_id_helper import get_external_trace_id -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value from libs.login import current_user, login_required diff --git a/api/controllers/console/app/generator.py b/api/controllers/console/app/generator.py index c720a5e074..7101d5df7b 100644 --- a/api/controllers/console/app/generator.py +++ b/api/controllers/console/app/generator.py @@ -1,6 +1,7 @@ from collections.abc import Sequence from flask_restx import Resource +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field from controllers.console import console_ns @@ -19,7 +20,6 @@ from core.helper.code_executor.python3.python3_code_provider import Python3CodeP from core.llm_generator.entities import RuleCodeGeneratePayload, RuleGeneratePayload, RuleStructuredOutputPayload from core.llm_generator.llm_generator import LLMGenerator from extensions.ext_database import db -from graphon.model_runtime.errors.invoke import InvokeError from libs.login import current_account_with_tenant, login_required from models import App from services.workflow_service import WorkflowService diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index d517f695b8..5b1abc98dc 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -18,6 +18,12 @@ from models.enums import AppMCPServerStatus from models.model import AppMCPServer +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value + + class MCPServerCreatePayload(BaseModel): description: str | None = Field(default=None, description="Server description") parameters: dict[str, Any] = Field(..., description="Server parameters configuration") @@ -30,25 +36,19 @@ class MCPServerUpdatePayload(BaseModel): status: str | None = Field(default=None, description="Server status") -def _to_timestamp(value: datetime | int | None) -> int | None: - if isinstance(value, datetime): - return int(value.timestamp()) - return value - - class AppMCPServerResponse(ResponseModel): id: str name: str server_code: str description: str - status: AppMCPServerStatus + status: str parameters: dict[str, Any] | list[Any] | str created_at: int | None = None updated_at: int | None = None @field_validator("parameters", mode="before") @classmethod - def _normalize_parameters(cls, value: Any) -> Any: + def _parse_json_string(cls, value: Any) -> Any: if isinstance(value, str): try: return json.loads(value) @@ -70,9 +70,7 @@ class AppMCPServerController(Resource): @console_ns.doc("get_app_mcp_server") @console_ns.doc(description="Get MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response( - 200, "MCP server configuration retrieved successfully", console_ns.models[AppMCPServerResponse.__name__] - ) + @console_ns.response(200, "Server configuration", console_ns.models[AppMCPServerResponse.__name__]) @login_required @account_initialization_required @setup_required @@ -87,9 +85,7 @@ class AppMCPServerController(Resource): @console_ns.doc(description="Create MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[MCPServerCreatePayload.__name__]) - @console_ns.response( - 201, "MCP server configuration created successfully", console_ns.models[AppMCPServerResponse.__name__] - ) + @console_ns.response(200, "Server created", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @account_initialization_required @get_app_model @@ -115,15 +111,13 @@ class AppMCPServerController(Resource): ) db.session.add(server) db.session.commit() - return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json"), 201 + return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json") @console_ns.doc("update_app_mcp_server") @console_ns.doc(description="Update MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[MCPServerUpdatePayload.__name__]) - @console_ns.response( - 200, "MCP server configuration updated successfully", console_ns.models[AppMCPServerResponse.__name__] - ) + @console_ns.response(200, "Server updated", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Server not found") @get_app_model @@ -160,7 +154,7 @@ class AppMCPServerRefreshController(Resource): @console_ns.doc("refresh_app_mcp_server") @console_ns.doc(description="Refresh MCP server configuration and regenerate server code") @console_ns.doc(params={"server_id": "Server ID"}) - @console_ns.response(200, "MCP server refreshed successfully", console_ns.models[AppMCPServerResponse.__name__]) + @console_ns.response(200, "Server refreshed", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Server not found") @setup_required diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 8571afde31..16cad35f1c 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -5,6 +5,11 @@ from typing import Any, Literal from flask import abort, request from flask_restx import Resource, fields, marshal, marshal_with +from graphon.enums import NodeType +from graphon.file import File +from graphon.file import helpers as file_helpers +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, ValidationError, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -37,18 +42,13 @@ from factories import file_factory, variable_factory from fields.member_fields import simple_account_fields from fields.online_user_fields import online_user_list_fields from fields.workflow_fields import workflow_fields, workflow_pagination_fields -from graphon.enums import NodeType -from graphon.file import File -from graphon.file import helpers as file_helpers -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs import helper from libs.datetime_utils import naive_utc_now from libs.helper import TimestampField, uuid_value from libs.login import current_account_with_tenant, login_required from models import App from models.model import AppMode -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow, WorkflowKind from repositories.workflow_collaboration_repository import WORKFLOW_ONLINE_USERS_PREFIX from services.app_generate_service import AppGenerateService from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError @@ -1139,7 +1139,7 @@ class WorkflowTypeConvertApi(Resource): def post(self, app_model: App): current_user, _ = current_account_with_tenant() args = WorkflowTypeConvertQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore - target_type = WorkflowType.value_of(args.target_type) + target_type = WorkflowKind.EVALUATION if args.target_type == "evaluation" else WorkflowKind.STANDARD workflow_service = WorkflowService() with Session(db.engine) as session: @@ -1163,6 +1163,7 @@ class WorkflowTypeConvertApi(Resource): "result": "success", "workflow_id": workflow.id, "type": workflow.type.value, + "kind": workflow.kind_or_standard, "updated_at": TimestampField().format(workflow.updated_at or workflow.created_at), } diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index 4b39590235..6b402898e8 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -4,6 +4,7 @@ from typing import Any from dateutil.parser import isoparse from flask import request from flask_restx import Resource +from graphon.enums import WorkflowExecutionStatus from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker @@ -15,7 +16,6 @@ from extensions.ext_database import db from fields.base import ResponseModel from fields.end_user_fields import SimpleEndUser from fields.member_fields import SimpleAccount -from graphon.enums import WorkflowExecutionStatus from libs.login import login_required from models import App from models.model import AppMode diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index 6748d95d6b..a1a075be71 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -3,6 +3,8 @@ from typing import Literal, TypedDict, cast from flask import request from flask_restx import Resource, fields, marshal_with +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from sqlalchemy.orm import sessionmaker @@ -26,8 +28,6 @@ from fields.workflow_run_fields import ( workflow_run_node_execution_list_fields, workflow_run_pagination_fields, ) -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus from libs.archive_storage import ArchiveStorageNotConfiguredError, get_archive_storage from libs.custom_inputs import time_duration from libs.helper import uuid_value diff --git a/api/controllers/console/auth/oauth_server.py b/api/controllers/console/auth/oauth_server.py index 727428c8e7..b55cda4244 100644 --- a/api/controllers/console/auth/oauth_server.py +++ b/api/controllers/console/auth/oauth_server.py @@ -5,11 +5,11 @@ from typing import Concatenate from flask import jsonify, request from flask.typing import ResponseReturnValue from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel from werkzeug.exceptions import BadRequest, NotFound from controllers.console.wraps import account_initialization_required, setup_required -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from models import Account from models.model import OAuthProviderApp diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 0b493d2c71..14ca27acbd 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -4,6 +4,7 @@ from urllib.parse import quote from flask import Response, request from flask_restx import Resource, fields, marshal, marshal_with +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field, field_validator from sqlalchemy import func, select from sqlalchemy.orm import Session @@ -53,7 +54,6 @@ from fields.dataset_fields import ( weighted_score_fields, ) from fields.document_fields import document_status_fields -from graphon.model_runtime.entities.model_entities import ModelType from libs.login import current_account_with_tenant, login_required from models import ApiToken, Dataset, Document, DocumentSegment, EvaluationRun, EvaluationTargetType, UploadFile from models.dataset import DatasetPermission, DatasetPermissionEnum diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 3372a967d9..98d4ad9412 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -3,19 +3,20 @@ import logging from argparse import ArgumentTypeError from collections.abc import Sequence from contextlib import ExitStack -from datetime import datetime from typing import Any, Literal, cast import sqlalchemy as sa from flask import request, send_file -from flask_restx import Resource, marshal -from pydantic import BaseModel, Field, field_validator +from flask_restx import Resource, fields, marshal, marshal_with +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from pydantic import BaseModel, Field from sqlalchemy import asc, desc, func, select from werkzeug.exceptions import Forbidden, NotFound import services from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload -from controllers.common.schema import register_schema_models +from controllers.common.schema import get_or_create_model, register_schema_models from controllers.console import console_ns from core.errors.error import ( LLMBadRequestError, @@ -30,14 +31,14 @@ from core.rag.extractor.entity.datasource_type import DatasourceType from core.rag.extractor.entity.extract_setting import ExtractSetting, NotionInfo, WebsiteInfo from core.rag.index_processor.constant.index_type import IndexTechniqueType from extensions.ext_database import db -from fields.base import ResponseModel +from fields.dataset_fields import dataset_fields from fields.document_fields import ( + dataset_and_document_fields, document_fields, + document_metadata_fields, document_status_fields, document_with_segments_fields, ) -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from libs.datetime_utils import naive_utc_now from libs.login import current_account_with_tenant, login_required from models import DatasetProcessRule, Document, DocumentSegment, UploadFile @@ -71,100 +72,27 @@ from ..wraps import ( logger = logging.getLogger(__name__) -def _to_timestamp(value: datetime | int | None) -> int | None: - if isinstance(value, datetime): - return int(value.timestamp()) - return value +# Register models for flask_restx to avoid dict type issues in Swagger +dataset_model = get_or_create_model("Dataset", dataset_fields) +document_metadata_model = get_or_create_model("DocumentMetadata", document_metadata_fields) -def _normalize_enum(value: Any) -> Any: - if isinstance(value, str) or value is None: - return value - return getattr(value, "value", value) +document_fields_copy = document_fields.copy() +document_fields_copy["doc_metadata"] = fields.List( + fields.Nested(document_metadata_model), attribute="doc_metadata_details" +) +document_model = get_or_create_model("Document", document_fields_copy) +document_with_segments_fields_copy = document_with_segments_fields.copy() +document_with_segments_fields_copy["doc_metadata"] = fields.List( + fields.Nested(document_metadata_model), attribute="doc_metadata_details" +) +document_with_segments_model = get_or_create_model("DocumentWithSegments", document_with_segments_fields_copy) -class DatasetResponse(ResponseModel): - id: str - name: str - description: str | None = None - permission: str | None = None - data_source_type: str | None = None - indexing_technique: str | None = None - created_by: str | None = None - created_at: int | None = None - - @field_validator("data_source_type", "indexing_technique", mode="before") - @classmethod - def _normalize_enum_fields(cls, value: Any) -> Any: - return _normalize_enum(value) - - @field_validator("created_at", mode="before") - @classmethod - def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: - return _to_timestamp(value) - - -class DocumentMetadataResponse(ResponseModel): - id: str - name: str - type: str - value: str | None = None - - -class DocumentResponse(ResponseModel): - id: str - position: int | None = None - data_source_type: str | None = None - data_source_info: Any = Field(default=None, validation_alias="data_source_info_dict") - data_source_detail_dict: Any = None - dataset_process_rule_id: str | None = None - name: str - created_from: str | None = None - created_by: str | None = None - created_at: int | None = None - tokens: int | None = None - indexing_status: str | None = None - error: str | None = None - enabled: bool | None = None - disabled_at: int | None = None - disabled_by: str | None = None - archived: bool | None = None - display_status: str | None = None - word_count: int | None = None - hit_count: int | None = None - doc_form: str | None = None - doc_metadata: list[DocumentMetadataResponse] = Field(default_factory=list, validation_alias="doc_metadata_details") - summary_index_status: str | None = None - need_summary: bool | None = None - - @field_validator("data_source_type", "indexing_status", "display_status", "doc_form", mode="before") - @classmethod - def _normalize_enum_fields(cls, value: Any) -> Any: - return _normalize_enum(value) - - @field_validator("doc_metadata", mode="before") - @classmethod - def _normalize_doc_metadata(cls, value: Any) -> list[Any]: - if value is None: - return [] - return value - - @field_validator("created_at", "disabled_at", mode="before") - @classmethod - def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: - return _to_timestamp(value) - - -class DocumentWithSegmentsResponse(DocumentResponse): - process_rule_dict: Any = None - completed_segments: int | None = None - total_segments: int | None = None - - -class DatasetAndDocumentResponse(ResponseModel): - dataset: DatasetResponse - documents: list[DocumentResponse] - batch: str +dataset_and_document_fields_copy = dataset_and_document_fields.copy() +dataset_and_document_fields_copy["dataset"] = fields.Nested(dataset_model) +dataset_and_document_fields_copy["documents"] = fields.List(fields.Nested(document_model)) +dataset_and_document_model = get_or_create_model("DatasetAndDocument", dataset_and_document_fields_copy) class DocumentRetryPayload(BaseModel): @@ -179,11 +107,6 @@ class GenerateSummaryPayload(BaseModel): document_list: list[str] -class DocumentMetadataUpdatePayload(BaseModel): - doc_type: str | None = None - doc_metadata: Any = None - - class DocumentDatasetListParam(BaseModel): page: int = Field(1, title="Page", description="Page number.") limit: int = Field(20, title="Limit", description="Page size.") @@ -201,13 +124,7 @@ register_schema_models( DocumentRetryPayload, DocumentRenamePayload, GenerateSummaryPayload, - DocumentMetadataUpdatePayload, DocumentBatchDownloadZipPayload, - DatasetResponse, - DocumentMetadataResponse, - DocumentResponse, - DocumentWithSegmentsResponse, - DatasetAndDocumentResponse, ) @@ -440,10 +357,10 @@ class DatasetDocumentListApi(Resource): @setup_required @login_required @account_initialization_required + @marshal_with(dataset_and_document_model) @cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_rate_limit_check("knowledge") @console_ns.expect(console_ns.models[KnowledgeConfig.__name__]) - @console_ns.response(200, "Documents created successfully", console_ns.models[DatasetAndDocumentResponse.__name__]) def post(self, dataset_id): current_user, _ = current_account_with_tenant() dataset_id = str(dataset_id) @@ -481,9 +398,7 @@ class DatasetDocumentListApi(Resource): except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() - return DatasetAndDocumentResponse.model_validate( - {"dataset": dataset, "documents": documents, "batch": batch}, from_attributes=True - ).model_dump(mode="json") + return {"dataset": dataset, "documents": documents, "batch": batch} @setup_required @login_required @@ -511,13 +426,12 @@ class DatasetInitApi(Resource): @console_ns.doc("init_dataset") @console_ns.doc(description="Initialize dataset with documents") @console_ns.expect(console_ns.models[KnowledgeConfig.__name__]) - @console_ns.response( - 201, "Dataset initialized successfully", console_ns.models[DatasetAndDocumentResponse.__name__] - ) + @console_ns.response(201, "Dataset initialized successfully", dataset_and_document_model) @console_ns.response(400, "Invalid request parameters") @setup_required @login_required @account_initialization_required + @marshal_with(dataset_and_document_model) @cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_rate_limit_check("knowledge") def post(self): @@ -565,9 +479,9 @@ class DatasetInitApi(Resource): except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() - return DatasetAndDocumentResponse.model_validate( - {"dataset": dataset, "documents": documents, "batch": batch}, from_attributes=True - ).model_dump(mode="json") + response = {"dataset": dataset, "documents": documents, "batch": batch} + + return response @console_ns.route("/datasets//documents//indexing-estimate") @@ -1074,7 +988,15 @@ class DocumentMetadataApi(DocumentResource): @console_ns.doc("update_document_metadata") @console_ns.doc(description="Update document metadata") @console_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"}) - @console_ns.expect(console_ns.models[DocumentMetadataUpdatePayload.__name__]) + @console_ns.expect( + console_ns.model( + "UpdateDocumentMetadataRequest", + { + "doc_type": fields.String(description="Document type"), + "doc_metadata": fields.Raw(description="Document metadata"), + }, + ) + ) @console_ns.response(200, "Document metadata updated successfully") @console_ns.response(404, "Document not found") @console_ns.response(403, "Permission denied") @@ -1087,10 +1009,10 @@ class DocumentMetadataApi(DocumentResource): document_id = str(document_id) document = self.get_document(dataset_id, document_id) - req_data = DocumentMetadataUpdatePayload.model_validate(request.get_json() or {}) + req_data = request.get_json() - doc_type = req_data.doc_type - doc_metadata = req_data.doc_metadata + doc_type = req_data.get("doc_type") + doc_metadata = req_data.get("doc_metadata") # The role of the current user in the ta table must be admin, owner, dataset_operator, or editor if not current_user.is_dataset_editor: @@ -1272,7 +1194,7 @@ class DocumentRenameApi(DocumentResource): @setup_required @login_required @account_initialization_required - @console_ns.response(200, "Document renamed successfully", console_ns.models[DocumentResponse.__name__]) + @marshal_with(document_model) @console_ns.expect(console_ns.models[DocumentRenamePayload.__name__]) def post(self, dataset_id, document_id): # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator @@ -1290,7 +1212,7 @@ class DocumentRenameApi(DocumentResource): except services.errors.document.DocumentIndexingError: raise DocumentIndexingError("Cannot delete document during indexing.") - return DocumentResponse.model_validate(document, from_attributes=True).model_dump(mode="json") + return document @console_ns.route("/datasets//documents//website-sync") diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index 2647bb1f5a..354c299bef 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -2,6 +2,7 @@ import uuid from flask import request from flask_restx import Resource, marshal +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field from sqlalchemy import String, cast, func, or_, select from sqlalchemy.dialects.postgresql import JSONB @@ -31,7 +32,6 @@ from core.rag.index_processor.constant.index_type import IndexTechniqueType from extensions.ext_database import db from extensions.ext_redis import redis_client from fields.segment_fields import child_chunk_fields, segment_fields -from graphon.model_runtime.entities.model_entities import ModelType from libs.helper import escape_like_pattern from libs.login import current_account_with_tenant, login_required from models.dataset import ChildChunk, DocumentSegment diff --git a/api/controllers/console/datasets/hit_testing_base.py b/api/controllers/console/datasets/hit_testing_base.py index 699fa599c8..8fb3699849 100644 --- a/api/controllers/console/datasets/hit_testing_base.py +++ b/api/controllers/console/datasets/hit_testing_base.py @@ -2,6 +2,7 @@ import logging from typing import Any from flask_restx import marshal +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field from werkzeug.exceptions import Forbidden, InternalServerError, NotFound @@ -20,7 +21,6 @@ from core.errors.error import ( QuotaExceededError, ) from fields.hit_testing_fields import hit_testing_record_fields -from graphon.model_runtime.errors.invoke import InvokeError from libs.login import current_user from models.account import Account from services.dataset_service import DatasetService diff --git a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py index fd0a8b33bc..bdf83b991e 100644 --- a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py +++ b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py @@ -2,6 +2,8 @@ from typing import Any from flask import make_response, redirect, request from flask_restx import Resource +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field from werkzeug.exceptions import Forbidden, NotFound @@ -10,8 +12,6 @@ from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required from core.plugin.impl.oauth import OAuthHandler -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from models.provider_ids import DatasourceProviderID from services.datasource_provider_service import DatasourceProviderService diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index b31d73f27d..3549f9542d 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -4,6 +4,7 @@ from typing import Any, NoReturn from flask import Response, request from flask_restx import Resource, marshal, marshal_with +from graphon.variables.types import SegmentType from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden @@ -27,7 +28,6 @@ from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTE from extensions.ext_database import db from factories.file_factory import build_from_mapping, build_from_mappings from factories.variable_factory import build_segment_with_type -from graphon.variables.types import SegmentType from libs.login import current_user, login_required from models import Account from models.dataset import Pipeline diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index ee146e8287..a8077d9eb0 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -4,6 +4,7 @@ from typing import Any, Literal, cast from flask import abort, request from flask_restx import Resource, marshal_with # type: ignore +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, ValidationError from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -40,7 +41,6 @@ from core.app.apps.pipeline.pipeline_generator import PipelineGenerator from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db from factories import variable_factory -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs import helper from libs.helper import TimestampField, UUIDStrOrEmpty from libs.login import current_account_with_tenant, current_user, login_required diff --git a/api/controllers/console/evaluation/evaluation.py b/api/controllers/console/evaluation/evaluation.py index 31490020c3..4ecccc05f1 100644 --- a/api/controllers/console/evaluation/evaluation.py +++ b/api/controllers/console/evaluation/evaluation.py @@ -152,6 +152,7 @@ available_evaluation_workflow_list_fields = { "app_id": fields.String, "app_name": fields.String, "type": fields.String, + "kind": fields.String, "version": fields.String, "marked_name": fields.String, "marked_comment": fields.String, @@ -743,6 +744,7 @@ class AvailableEvaluationWorkflowsApi(Resource): "app_id": wf.app_id, "app_name": app_names.get(wf.app_id, ""), "type": wf.type.value, + "kind": wf.kind_or_standard, "version": wf.version, "marked_name": wf.marked_name, "marked_comment": wf.marked_comment, diff --git a/api/controllers/console/explore/audio.py b/api/controllers/console/explore/audio.py index ab660d9dc3..a37077af42 100644 --- a/api/controllers/console/explore/audio.py +++ b/api/controllers/console/explore/audio.py @@ -1,6 +1,7 @@ import logging from flask import request +from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError import services @@ -19,7 +20,6 @@ from controllers.console.app.error import ( ) from controllers.console.explore.wraps import InstalledAppResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError from services.audio_service import AudioService from services.errors.audio import ( AudioTooLargeServiceError, diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index ccdccceaa6..eacd7332fe 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -2,6 +2,7 @@ import logging from typing import Any, Literal from uuid import UUID +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound @@ -25,7 +26,6 @@ from core.errors.error import ( QuotaExceededError, ) from extensions.ext_database import db -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.datetime_utils import naive_utc_now from libs.login import current_user diff --git a/api/controllers/console/explore/message.py b/api/controllers/console/explore/message.py index 209667d1d0..64d55d7ca3 100644 --- a/api/controllers/console/explore/message.py +++ b/api/controllers/console/explore/message.py @@ -2,6 +2,7 @@ import logging from typing import Literal from flask import request +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound @@ -24,7 +25,6 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from fields.conversation_fields import ResultResponse from fields.message_fields import MessageInfiniteScrollPagination, MessageListItem, SuggestedQuestionsResponse -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.login import current_account_with_tenant from models.enums import FeedbackRating diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 1456301a24..0a3595454a 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -3,6 +3,8 @@ from typing import Any, Literal, cast from flask import request from flask_restx import Resource, fields, marshal, marshal_with +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel from sqlalchemy import select from werkzeug.exceptions import Forbidden, InternalServerError, NotFound @@ -59,8 +61,6 @@ from fields.workflow_fields import ( workflow_fields, workflow_partial_fields, ) -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value from libs.login import current_user diff --git a/api/controllers/console/explore/workflow.py b/api/controllers/console/explore/workflow.py index 438cce4fd8..da88de6776 100644 --- a/api/controllers/console/explore/workflow.py +++ b/api/controllers/console/explore/workflow.py @@ -1,5 +1,7 @@ import logging +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError from controllers.common.controller_schemas import WorkflowRunPayload @@ -21,8 +23,6 @@ from core.errors.error import ( QuotaExceededError, ) from extensions.ext_redis import redis_client -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.login import current_account_with_tenant from models.model import AppMode, InstalledApp diff --git a/api/controllers/console/remote_files.py b/api/controllers/console/remote_files.py index 2a46d2250a..551c86fd82 100644 --- a/api/controllers/console/remote_files.py +++ b/api/controllers/console/remote_files.py @@ -2,6 +2,7 @@ import urllib.parse import httpx from flask_restx import Resource +from graphon.file import helpers as file_helpers from pydantic import BaseModel, Field import services @@ -15,7 +16,6 @@ from controllers.console import console_ns from core.helper import ssrf_proxy from extensions.ext_database import db from fields.file_fields import FileWithSignedUrl, RemoteFileInfo -from graphon.file import helpers as file_helpers from libs.login import current_account_with_tenant, login_required from services.file_service import FileService diff --git a/api/controllers/console/workspace/agent_providers.py b/api/controllers/console/workspace/agent_providers.py index 764f488755..3fdcbc4710 100644 --- a/api/controllers/console/workspace/agent_providers.py +++ b/api/controllers/console/workspace/agent_providers.py @@ -1,8 +1,8 @@ from flask_restx import Resource, fields +from graphon.model_runtime.utils.encoders import jsonable_encoder from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, setup_required -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from services.agent_service import AgentService diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py index f45b72f390..b6b9deb1f9 100644 --- a/api/controllers/console/workspace/endpoint.py +++ b/api/controllers/console/workspace/endpoint.py @@ -2,13 +2,13 @@ from typing import Any from flask import request from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required from core.plugin.impl.exc import PluginPermissionDeniedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from services.plugin.endpoint_service import EndpointService diff --git a/api/controllers/console/workspace/load_balancing_config.py b/api/controllers/console/workspace/load_balancing_config.py index 2a6f37aec8..e4cfca9fa4 100644 --- a/api/controllers/console/workspace/load_balancing_config.py +++ b/api/controllers/console/workspace/load_balancing_config.py @@ -1,12 +1,12 @@ from flask_restx import Resource +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic import BaseModel from werkzeug.exceptions import Forbidden from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, setup_required -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from libs.login import current_account_with_tenant, login_required from models import TenantAccountRole from services.model_load_balancing_service import ModelLoadBalancingService diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index 4b10561fdb..cbb9677309 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -3,13 +3,13 @@ from typing import Any, Literal from flask import request, send_file from flask_restx import Resource +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import uuid_value from libs.login import current_account_with_tenant, login_required from services.billing_service import BillingService diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py index b2d07ff8f9..f8f95304f0 100644 --- a/api/controllers/console/workspace/models.py +++ b/api/controllers/console/workspace/models.py @@ -3,14 +3,14 @@ from typing import Any, cast from flask import request from flask_restx import Resource +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from controllers.common.schema import register_enum_models, register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import uuid_value from libs.login import current_account_with_tenant, login_required from services.model_load_balancing_service import ModelLoadBalancingService diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index b3e344ccea..aa674a63b3 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -4,6 +4,7 @@ from typing import Any, Literal from flask import request, send_file from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field from werkzeug.datastructures import FileStorage from werkzeug.exceptions import Forbidden @@ -14,7 +15,6 @@ from controllers.console import console_ns from controllers.console.workspace import plugin_permission_required from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required from core.plugin.impl.exc import PluginDaemonClientSideError -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 471594f349..c9956501e2 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -5,6 +5,7 @@ from urllib.parse import urlparse from flask import make_response, redirect, request, send_file from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden @@ -27,7 +28,6 @@ from core.plugin.entities.plugin_daemon import CredentialType from core.plugin.impl.oauth import OAuthHandler from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration from extensions.ext_database import db -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import alphanumeric, uuid_value from libs.login import current_account_with_tenant, login_required from models.provider_ids import ToolProviderID diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index d11b66244f..7a28a09861 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -3,6 +3,7 @@ from typing import Any from flask import make_response, redirect, request from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, model_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden @@ -15,7 +16,6 @@ from core.plugin.impl.oauth import OAuthHandler from core.trigger.entities.entities import SubscriptionBuilderUpdater from core.trigger.trigger_manager import TriggerManager from extensions.ext_database import db -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_user, login_required from models.account import Account from models.provider_ids import TriggerProviderID diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py index 72cab3de73..83c8fa02fe 100644 --- a/api/controllers/inner_api/plugin/plugin.py +++ b/api/controllers/inner_api/plugin/plugin.py @@ -1,4 +1,5 @@ from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from controllers.console.wraps import setup_required from controllers.inner_api import inner_api_ns @@ -29,7 +30,6 @@ from core.plugin.entities.request import ( ) from core.tools.entities.tool_entities import ToolProviderType from core.tools.signature import get_signed_file_url_for_plugin -from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import length_prefixed_response from models import Account, Tenant from models.model import EndUser diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py index 2f309262cb..a5846e2815 100644 --- a/api/controllers/inner_api/plugin/wraps.py +++ b/api/controllers/inner_api/plugin/wraps.py @@ -20,13 +20,10 @@ class TenantUserPayload(BaseModel): def get_user(tenant_id: str, user_id: str | None) -> EndUser: """ - Get current user. + Get current user NOTE: user_id is not trusted, it could be maliciously set to any value. - As a result, it could only be considered as an end user id. Even when a - concrete end-user ID is supplied, lookups must stay tenant-scoped so one - tenant cannot bind another tenant's user record into the plugin request - context. + As a result, it could only be considered as an end user id. """ if not user_id: user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID @@ -45,14 +42,7 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser: .limit(1) ) else: - user_model = session.scalar( - select(EndUser) - .where( - EndUser.id == user_id, - EndUser.tenant_id == tenant_id, - ) - .limit(1) - ) + user_model = session.get(EndUser, user_id) if not user_model: user_model = EndUser( diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index f652bbc581..8066f198bb 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -2,6 +2,7 @@ from typing import Any, Union from flask import Response from flask_restx import Resource +from graphon.variables.input_entities import VariableEntity, VariableEntityType from pydantic import BaseModel, Field, ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -11,7 +12,6 @@ from controllers.mcp import mcp_ns from core.mcp import types as mcp_types from core.mcp.server.streamable_http import handle_mcp_request from extensions.ext_database import db -from graphon.variables.input_entities import VariableEntity, VariableEntityType from libs import helper from models.enums import AppMCPServerStatus from models.model import App, AppMCPServer, AppMode, EndUser diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py index e818573b8f..907dd1b06d 100644 --- a/api/controllers/service_api/app/audio.py +++ b/api/controllers/service_api/app/audio.py @@ -2,6 +2,7 @@ import logging from flask import request from flask_restx import Resource +from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError import services @@ -21,7 +22,6 @@ from controllers.service_api.app.error import ( ) from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError from models.model import App, EndUser from services.audio_service import AudioService from services.errors.audio import ( diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index 31f2797d66..3142e5118e 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -4,6 +4,7 @@ from uuid import UUID from flask import request from flask_restx import Resource +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import BadRequest, InternalServerError, NotFound @@ -28,7 +29,6 @@ from core.errors.error import ( QuotaExceededError, ) from core.helper.trace_id_helper import get_external_trace_id -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import UUIDStrOrEmpty from models.model import App, AppMode, EndUser diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index ca4b18cb5e..47e1889c95 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -3,6 +3,7 @@ from typing import Any, Literal from flask import request from flask_restx import Resource +from graphon.variables.types import SegmentType from pydantic import BaseModel, Field, TypeAdapter, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound @@ -21,7 +22,6 @@ from fields.conversation_fields import ( ConversationInfiniteScrollPagination, SimpleConversation, ) -from graphon.variables.types import SegmentType from libs.helper import UUIDStrOrEmpty from models.model import App, AppMode, EndUser from services.conversation_service import ConversationService diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index 76519cad0a..fd954be6b1 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -2,6 +2,7 @@ from typing import Any, Literal, cast from flask import request from flask_restx import marshal +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field, TypeAdapter, field_validator from werkzeug.exceptions import Forbidden, NotFound @@ -18,7 +19,6 @@ from core.plugin.impl.model_runtime_factory import create_plugin_provider_manage from core.rag.index_processor.constant.index_type import IndexTechniqueType from fields.dataset_fields import dataset_detail_fields from fields.tag_fields import DataSetTag -from graphon.model_runtime.entities.model_entities import ModelType from libs.login import current_user from models.account import Account from models.dataset import DatasetPermissionEnum diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index 5992fa7410..971b63577c 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -2,6 +2,7 @@ from typing import Any from flask import request from flask_restx import marshal +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field from sqlalchemy import select from werkzeug.exceptions import NotFound @@ -22,7 +23,6 @@ from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexTechniqueType from extensions.ext_database import db from fields.segment_fields import child_chunk_fields, segment_fields -from graphon.model_runtime.entities.model_entities import ModelType from libs.login import current_account_with_tenant from models.dataset import Dataset from services.dataset_service import DatasetService, DocumentService, SegmentService diff --git a/api/controllers/service_api/workspace/models.py b/api/controllers/service_api/workspace/models.py index 5ac65fc4e6..c0a6cb0a76 100644 --- a/api/controllers/service_api/workspace/models.py +++ b/api/controllers/service_api/workspace/models.py @@ -1,9 +1,9 @@ from flask_login import current_user from flask_restx import Resource +from graphon.model_runtime.utils.encoders import jsonable_encoder from controllers.service_api import service_api_ns from controllers.service_api.wraps import validate_dataset_token -from graphon.model_runtime.utils.encoders import jsonable_encoder from services.model_provider_service import ModelProviderService diff --git a/api/controllers/web/audio.py b/api/controllers/web/audio.py index 3ad595f1f4..0ef4471018 100644 --- a/api/controllers/web/audio.py +++ b/api/controllers/web/audio.py @@ -2,6 +2,7 @@ import logging from flask import request from flask_restx import fields, marshal_with +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import field_validator from werkzeug.exceptions import InternalServerError @@ -21,7 +22,6 @@ from controllers.web.error import ( ) from controllers.web.wraps import WebApiResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError from libs.helper import uuid_value from models.model import App from services.audio_service import AudioService diff --git a/api/controllers/web/completion.py b/api/controllers/web/completion.py index 0528184d79..e37f9af5f0 100644 --- a/api/controllers/web/completion.py +++ b/api/controllers/web/completion.py @@ -1,6 +1,7 @@ import logging from typing import Any, Literal +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound @@ -25,7 +26,6 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value from models.model import AppMode diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py index 07ecf8035b..39afdd843f 100644 --- a/api/controllers/web/message.py +++ b/api/controllers/web/message.py @@ -2,6 +2,7 @@ import logging from typing import Literal from flask import request +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound @@ -23,7 +24,6 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from fields.conversation_fields import ResultResponse from fields.message_fields import SuggestedQuestionsResponse, WebMessageInfiniteScrollPagination, WebMessageListItem -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from models.enums import FeedbackRating from models.model import AppMode diff --git a/api/controllers/web/remote_files.py b/api/controllers/web/remote_files.py index fe31e9d4ac..38aeccc642 100644 --- a/api/controllers/web/remote_files.py +++ b/api/controllers/web/remote_files.py @@ -1,6 +1,7 @@ import urllib.parse import httpx +from graphon.file import helpers as file_helpers from pydantic import BaseModel, Field, HttpUrl import services @@ -13,7 +14,6 @@ from controllers.common.errors import ( from core.helper import ssrf_proxy from extensions.ext_database import db from fields.file_fields import FileWithSignedUrl, RemoteFileInfo -from graphon.file import helpers as file_helpers from services.file_service import FileService from ..common.schema import register_schema_models diff --git a/api/controllers/web/workflow.py b/api/controllers/web/workflow.py index 98211193a0..796e090976 100644 --- a/api/controllers/web/workflow.py +++ b/api/controllers/web/workflow.py @@ -1,5 +1,7 @@ import logging +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError from controllers.common.controller_schemas import WorkflowRunPayload @@ -22,8 +24,6 @@ from core.errors.error import ( QuotaExceededError, ) from extensions.ext_redis import redis_client -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService diff --git a/api/core/agent/cot_agent_runner.py b/api/core/agent/cot_agent_runner.py index 0bc93ad34d..f07ac64498 100644 --- a/api/core/agent/cot_agent_runner.py +++ b/api/core/agent/cot_agent_runner.py @@ -4,6 +4,15 @@ from abc import ABC, abstractmethod from collections.abc import Generator, Mapping, Sequence from typing import Any, TypedDict +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + PromptMessage, + PromptMessageTool, + ToolPromptMessage, + UserPromptMessage, +) + from core.agent.base_agent_runner import BaseAgentRunner from core.agent.entities import AgentScratchpadUnit from core.agent.errors import AgentMaxIterationError @@ -15,14 +24,6 @@ from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransfo from core.tools.__base.tool import Tool from core.tools.entities.tool_entities import ToolInvokeMeta from core.tools.tool_engine import ToolEngine -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - PromptMessage, - PromptMessageTool, - ToolPromptMessage, - UserPromptMessage, -) from models.model import Message logger = logging.getLogger(__name__) diff --git a/api/core/agent/cot_chat_agent_runner.py b/api/core/agent/cot_chat_agent_runner.py index a2186be100..2b2e26987e 100644 --- a/api/core/agent/cot_chat_agent_runner.py +++ b/api/core/agent/cot_chat_agent_runner.py @@ -1,6 +1,5 @@ import json -from core.agent.cot_agent_runner import CotAgentRunner from graphon.file import file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -12,6 +11,8 @@ from graphon.model_runtime.entities import ( from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes from graphon.model_runtime.utils.encoders import jsonable_encoder +from core.agent.cot_agent_runner import CotAgentRunner + class CotChatAgentRunner(CotAgentRunner): def _organize_system_prompt(self) -> SystemPromptMessage: diff --git a/api/core/agent/cot_completion_agent_runner.py b/api/core/agent/cot_completion_agent_runner.py index 51a30998ae..d4c52a8eb1 100644 --- a/api/core/agent/cot_completion_agent_runner.py +++ b/api/core/agent/cot_completion_agent_runner.py @@ -1,6 +1,5 @@ import json -from core.agent.cot_agent_runner import CotAgentRunner from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, PromptMessage, @@ -9,6 +8,8 @@ from graphon.model_runtime.entities.message_entities import ( ) from graphon.model_runtime.utils.encoders import jsonable_encoder +from core.agent.cot_agent_runner import CotAgentRunner + class CotCompletionAgentRunner(CotAgentRunner): def _organize_instruction_prompt(self) -> str: diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py index 29de0b8b1c..b31fde5fb2 100644 --- a/api/core/agent/fc_agent_runner.py +++ b/api/core/agent/fc_agent_runner.py @@ -4,13 +4,6 @@ from collections.abc import Generator from copy import deepcopy from typing import Any, Union -from core.agent.base_agent_runner import BaseAgentRunner -from core.agent.errors import AgentMaxIterationError -from core.app.apps.base_app_queue_manager import PublishFrom -from core.app.entities.queue_entities import QueueAgentThoughtEvent, QueueMessageEndEvent, QueueMessageFileEvent -from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform -from core.tools.entities.tool_entities import ToolInvokeMeta -from core.tools.tool_engine import ToolEngine from graphon.file import file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -26,6 +19,14 @@ from graphon.model_runtime.entities import ( UserPromptMessage, ) from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes + +from core.agent.base_agent_runner import BaseAgentRunner +from core.agent.errors import AgentMaxIterationError +from core.app.apps.base_app_queue_manager import PublishFrom +from core.app.entities.queue_entities import QueueAgentThoughtEvent, QueueMessageEndEvent, QueueMessageFileEvent +from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform +from core.tools.entities.tool_entities import ToolInvokeMeta +from core.tools.tool_engine import ToolEngine from models.model import Message logger = logging.getLogger(__name__) diff --git a/api/core/agent/output_parser/cot_output_parser.py b/api/core/agent/output_parser/cot_output_parser.py index f341ca5a0b..8cccd2be6d 100644 --- a/api/core/agent/output_parser/cot_output_parser.py +++ b/api/core/agent/output_parser/cot_output_parser.py @@ -3,9 +3,10 @@ import re from collections.abc import Generator from typing import Any, Union -from core.agent.entities import AgentScratchpadUnit from graphon.model_runtime.entities.llm_entities import LLMResultChunk +from core.agent.entities import AgentScratchpadUnit + class CotAgentOutputParser: @classmethod diff --git a/api/core/app/app_config/easy_ui_based_app/model_config/manager.py b/api/core/app/app_config/easy_ui_based_app/model_config/manager.py index 02498c23e1..9d980e5ca3 100644 --- a/api/core/app/app_config/easy_ui_based_app/model_config/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/model_config/manager.py @@ -1,9 +1,10 @@ from collections.abc import Mapping from typing import Any +from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType + from core.app.app_config.entities import ModelConfigEntity from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType from models.model import AppModelConfigDict from models.provider_ids import ModelProviderID diff --git a/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py index 4c07445df3..57c6d1c496 100644 --- a/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py @@ -1,5 +1,7 @@ from typing import Any +from graphon.model_runtime.entities.message_entities import PromptMessageRole + from core.app.app_config.entities import ( AdvancedChatMessageEntity, AdvancedChatPromptTemplateEntity, @@ -7,7 +9,6 @@ from core.app.app_config.entities import ( PromptTemplateEntity, ) from core.prompt.simple_prompt_transform import ModelMode -from graphon.model_runtime.entities.message_entities import PromptMessageRole from models.model import AppMode, AppModelConfigDict diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index 53563dc5da..819aca864c 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -1,14 +1,14 @@ from enum import StrEnum, auto from typing import Any, Literal -from pydantic import BaseModel, Field - -from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict -from core.rag.entities import MetadataFilteringCondition from graphon.file import FileUploadConfig from graphon.model_runtime.entities.llm_entities import LLMMode from graphon.model_runtime.entities.message_entities import PromptMessageRole from graphon.variables.input_entities import VariableEntity as WorkflowVariableEntity +from pydantic import BaseModel, Field + +from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict +from core.rag.entities import MetadataFilteringCondition from models.model import AppMode diff --git a/api/core/app/app_config/features/file_upload/manager.py b/api/core/app/app_config/features/file_upload/manager.py index 8f20ef2ff9..959c3868b4 100644 --- a/api/core/app/app_config/features/file_upload/manager.py +++ b/api/core/app/app_config/features/file_upload/manager.py @@ -1,9 +1,10 @@ from collections.abc import Mapping from typing import Any -from constants import DEFAULT_FILE_NUMBER_LIMITS from graphon.file import FileUploadConfig +from constants import DEFAULT_FILE_NUMBER_LIMITS + class FileUploadConfigManager: @classmethod diff --git a/api/core/app/app_config/workflow_ui_based_app/variables/manager.py b/api/core/app/app_config/workflow_ui_based_app/variables/manager.py index 13ace32fd6..62e0c31d1a 100644 --- a/api/core/app/app_config/workflow_ui_based_app/variables/manager.py +++ b/api/core/app/app_config/workflow_ui_based_app/variables/manager.py @@ -1,7 +1,8 @@ import re -from core.app.app_config.entities import RagPipelineVariableEntity from graphon.variables.input_entities import VariableEntity + +from core.app.app_config.entities import RagPipelineVariableEntity from models.workflow import Workflow diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 9e64b471cb..985ded0f74 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -18,6 +18,11 @@ from constants import UUID_NIL if TYPE_CHECKING: from controllers.console.app.workflow import LoopNodeRunPayload +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from graphon.runtime import GraphRuntimeState +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader + from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner @@ -43,10 +48,6 @@ from core.repositories import DifyCoreRepositoryFactory from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from graphon.runtime import GraphRuntimeState -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from libs.flask_utils import preserve_flask_contexts from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index 4e57b4dedc..7b4cb98bd4 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -3,6 +3,12 @@ import time from collections.abc import Mapping, Sequence from typing import Any, cast +from graphon.enums import WorkflowType +from graphon.graph_engine.command_channels import RedisChannel +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import VariableLoader +from graphon.variables.variables import Variable from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -37,12 +43,6 @@ from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.otel import WorkflowAppRunnerHandler, trace_span -from graphon.enums import WorkflowType -from graphon.graph_engine.command_channels import RedisChannel -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import VariableLoader -from graphon.variables.variables import Variable from models import Workflow from models.model import App, Conversation, Message, MessageAnnotation from models.workflow import ConversationVariable diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index 5cdc477028..5872f6b264 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -6,6 +6,7 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, overload from flask import Flask, current_app +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from configs import dify_config @@ -23,7 +24,6 @@ from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, In from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser from services.conversation_service import ConversationService diff --git a/api/core/app/apps/base_app_generate_response_converter.py b/api/core/app/apps/base_app_generate_response_converter.py index d5edfaeb25..406d07927e 100644 --- a/api/core/app/apps/base_app_generate_response_converter.py +++ b/api/core/app/apps/base_app_generate_response_converter.py @@ -3,10 +3,11 @@ from abc import ABC, abstractmethod from collections.abc import Generator, Mapping from typing import Any, Union +from graphon.model_runtime.errors.invoke import InvokeError + from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.task_entities import AppBlockingResponse, AppStreamResponse from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/base_app_queue_manager.py b/api/core/app/apps/base_app_queue_manager.py index d1771452c5..20bf81aeec 100644 --- a/api/core/app/apps/base_app_queue_manager.py +++ b/api/core/app/apps/base_app_queue_manager.py @@ -7,6 +7,7 @@ from enum import IntEnum, auto from typing import Any from cachetools import TTLCache, cachedmethod +from graphon.runtime import GraphRuntimeState from redis.exceptions import RedisError from sqlalchemy.orm import DeclarativeMeta @@ -21,7 +22,6 @@ from core.app.entities.queue_entities import ( WorkflowQueueMessage, ) from extensions.ext_redis import redis_client -from graphon.runtime import GraphRuntimeState logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index 1251b397e2..4aebc0cb30 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -5,6 +5,17 @@ from collections.abc import Generator, Mapping, Sequence from mimetypes import guess_extension from typing import TYPE_CHECKING, Any, Union +from graphon.file import FileTransferMethod, FileType +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + ImagePromptMessageContent, + PromptMessage, + TextPromptMessageContent, +) +from graphon.model_runtime.entities.model_entities import ModelPropertyKey +from graphon.model_runtime.errors.invoke import InvokeBadRequestError + from core.app.app_config.entities import ExternalDataVariableEntity, PromptTemplateEntity from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom from core.app.entities.app_invoke_entities import ( @@ -30,16 +41,6 @@ from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, Comp from core.prompt.simple_prompt_transform import ModelMode, SimplePromptTransform from core.tools.tool_file_manager import ToolFileManager from extensions.ext_database import db -from graphon.file import FileTransferMethod, FileType -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - ImagePromptMessageContent, - PromptMessage, - TextPromptMessageContent, -) -from graphon.model_runtime.entities.model_entities import ModelPropertyKey -from graphon.model_runtime.errors.invoke import InvokeBadRequestError from models.enums import CreatorUserRole, MessageFileBelongsTo from models.model import App, AppMode, Message, MessageAnnotation, MessageFile diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index 58afefe296..891dcece73 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -6,6 +6,7 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, overload from flask import Flask, copy_current_request_context, current_app +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from configs import dify_config @@ -23,7 +24,6 @@ from core.app.entities.app_invoke_entities import ChatAppGenerateEntity, InvokeF from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from models import Account from models.model import App, EndUser from services.conversation_service import ConversationService diff --git a/api/core/app/apps/chat/app_runner.py b/api/core/app/apps/chat/app_runner.py index 077c5239f3..050f763e95 100644 --- a/api/core/app/apps/chat/app_runner.py +++ b/api/core/app/apps/chat/app_runner.py @@ -1,6 +1,8 @@ import logging from typing import cast +from graphon.file import File +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom @@ -16,8 +18,6 @@ from core.model_manager import ModelInstance from core.moderation.base import ModerationError from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from extensions.ext_database import db -from graphon.file import File -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.model import App, Conversation, Message logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/common/graph_runtime_state_support.py b/api/core/app/apps/common/graph_runtime_state_support.py index 2a90fbdad0..ab277857fe 100644 --- a/api/core/app/apps/common/graph_runtime_state_support.py +++ b/api/core/app/apps/common/graph_runtime_state_support.py @@ -4,9 +4,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -from core.workflow.system_variables import SystemVariableKey, get_system_text from graphon.runtime import GraphRuntimeState +from core.workflow.system_variables import SystemVariableKey, get_system_text + if TYPE_CHECKING: from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline diff --git a/api/core/app/apps/common/workflow_response_converter.py b/api/core/app/apps/common/workflow_response_converter.py index bd685d5189..a515531616 100644 --- a/api/core/app/apps/common/workflow_response_converter.py +++ b/api/core/app/apps/common/workflow_response_converter.py @@ -6,6 +6,19 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, NewType, TypedDict, Union +from graphon.entities import WorkflowStartReason +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import ( + BuiltinNodeTypes, + WorkflowExecutionStatus, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) +from graphon.file import FILE_MODEL_IDENTITY, File +from graphon.runtime import GraphRuntimeState +from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment +from graphon.variables.variables import Variable +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import select from sqlalchemy.orm import Session @@ -55,19 +68,6 @@ from core.workflow.human_input_forms import load_form_tokens_by_form_id from core.workflow.system_variables import SystemVariableKey, system_variables_to_mapping from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db -from graphon.entities import WorkflowStartReason -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import ( - BuiltinNodeTypes, - WorkflowExecutionStatus, - WorkflowNodeExecutionMetadataKey, - WorkflowNodeExecutionStatus, -) -from graphon.file import FILE_MODEL_IDENTITY, File -from graphon.runtime import GraphRuntimeState -from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment -from graphon.variables.variables import Variable -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.datetime_utils import naive_utc_now from models import Account, EndUser from models.human_input import HumanInputForm diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index 423bfdac51..61339b316a 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -6,6 +6,7 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, overload from flask import Flask, copy_current_request_context, current_app +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from sqlalchemy import select @@ -23,7 +24,6 @@ from core.app.entities.app_invoke_entities import CompletionAppGenerateEntity, I from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from models import Account, App, EndUser, Message from services.errors.app import MoreLikeThisDisabledError from services.errors.message import MessageNotExistsError diff --git a/api/core/app/apps/completion/app_runner.py b/api/core/app/apps/completion/app_runner.py index 6bb1ecdcb1..b216f7cf7b 100644 --- a/api/core/app/apps/completion/app_runner.py +++ b/api/core/app/apps/completion/app_runner.py @@ -1,6 +1,8 @@ import logging from typing import cast +from graphon.file import File +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager @@ -14,8 +16,6 @@ from core.model_manager import ModelInstance from core.moderation.base import ModerationError from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from extensions.ext_database import db -from graphon.file import File -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.model import App, Message logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/pipeline/pipeline_generator.py b/api/core/app/apps/pipeline/pipeline_generator.py index 4b2f17189b..83c74b86e5 100644 --- a/api/core/app/apps/pipeline/pipeline_generator.py +++ b/api/core/app/apps/pipeline/pipeline_generator.py @@ -10,6 +10,8 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, cast, overload from flask import Flask, current_app +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from pydantic import ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -41,8 +43,6 @@ from core.repositories.factory import ( WorkflowNodeExecutionRepository, ) from extensions.ext_database import db -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from libs.flask_utils import preserve_flask_contexts from models import Account, EndUser, Workflow, WorkflowNodeExecutionTriggeredFrom from models.dataset import Document, DocumentPipelineExecutionLog, Pipeline diff --git a/api/core/app/apps/pipeline/pipeline_runner.py b/api/core/app/apps/pipeline/pipeline_runner.py index 2ee0ae27eb..36daaf09e9 100644 --- a/api/core/app/apps/pipeline/pipeline_runner.py +++ b/api/core/app/apps/pipeline/pipeline_runner.py @@ -2,6 +2,12 @@ import logging import time from typing import cast +from graphon.enums import WorkflowType +from graphon.graph import Graph +from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import VariableLoader +from graphon.variables.variables import RAGPipelineVariable, RAGPipelineVariableInput from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager @@ -20,12 +26,6 @@ from core.workflow.system_variables import build_bootstrap_variables, build_syst from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db -from graphon.enums import WorkflowType -from graphon.graph import Graph -from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import VariableLoader -from graphon.variables.variables import RAGPipelineVariable, RAGPipelineVariableInput from models.dataset import Document, Pipeline from models.model import EndUser from models.workflow import Workflow diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 5a1e7e117f..3421a13133 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -8,6 +8,10 @@ from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any, Literal, overload from flask import Flask, current_app +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from graphon.runtime import GraphRuntimeState +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from pydantic import ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -34,10 +38,6 @@ from core.repositories import DifyCoreRepositoryFactory from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from graphon.runtime import GraphRuntimeState -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from libs.flask_utils import preserve_flask_contexts from models.account import Account from models.enums import WorkflowRunTriggeredFrom @@ -57,7 +57,7 @@ class WorkflowAppGenerator(BaseAppGenerator): @staticmethod def _ensure_snippet_start_node_in_worker(*, session: Session, workflow: Workflow) -> Workflow: """Re-apply snippet virtual Start injection after worker reloads workflow from DB.""" - if workflow.type != "snippet": + if workflow.kind_or_standard != "snippet": return workflow from models.snippet import CustomizedSnippet diff --git a/api/core/app/apps/workflow/app_runner.py b/api/core/app/apps/workflow/app_runner.py index cfb9208486..2cb8088971 100644 --- a/api/core/app/apps/workflow/app_runner.py +++ b/api/core/app/apps/workflow/app_runner.py @@ -3,6 +3,12 @@ import time from collections.abc import Sequence from typing import cast +from graphon.enums import WorkflowType +from graphon.graph_engine.command_channels import RedisChannel +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import VariableLoader + from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow.app_config_manager import WorkflowAppConfig from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner @@ -15,11 +21,6 @@ from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_redis import redis_client from extensions.otel import WorkflowAppRunnerHandler, trace_span -from graphon.enums import WorkflowType -from graphon.graph_engine.command_channels import RedisChannel -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import VariableLoader from libs.datetime_utils import naive_utc_now from models.workflow import Workflow diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 15645add57..96387133b1 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -4,6 +4,9 @@ from collections.abc import Callable, Generator from contextlib import contextmanager from typing import Union +from graphon.entities import WorkflowStartReason +from graphon.enums import WorkflowExecutionStatus +from graphon.runtime import GraphRuntimeState from sqlalchemy.orm import Session, sessionmaker from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME @@ -58,9 +61,6 @@ from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk from core.ops.ops_trace_manager import TraceQueueManager from core.workflow.system_variables import build_system_variables from extensions.ext_database import db -from graphon.entities import WorkflowStartReason -from graphon.enums import WorkflowExecutionStatus -from graphon.runtime import GraphRuntimeState from models import Account from models.enums import CreatorUserRole from models.model import EndUser diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 047b54c86c..437432611d 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -3,6 +3,39 @@ import time from collections.abc import Mapping, Sequence from typing import Any, cast +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.entities.pause_reason import HumanInputRequired +from graphon.graph import Graph +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import ( + GraphEngineEvent, + GraphRunAbortedEvent, + GraphRunFailedEvent, + GraphRunPartialSucceededEvent, + GraphRunPausedEvent, + GraphRunStartedEvent, + GraphRunSucceededEvent, + NodeRunAgentLogEvent, + NodeRunExceptionEvent, + NodeRunFailedEvent, + NodeRunHumanInputFormFilledEvent, + NodeRunHumanInputFormTimeoutEvent, + NodeRunIterationFailedEvent, + NodeRunIterationNextEvent, + NodeRunIterationStartedEvent, + NodeRunIterationSucceededEvent, + NodeRunLoopFailedEvent, + NodeRunLoopNextEvent, + NodeRunLoopStartedEvent, + NodeRunLoopSucceededEvent, + NodeRunRetrieverResourceEvent, + NodeRunRetryEvent, + NodeRunStartedEvent, + NodeRunStreamChunkEvent, + NodeRunSucceededEvent, +) +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from pydantic import ValidationError from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom @@ -49,39 +82,6 @@ from core.workflow.system_variables import ( from core.workflow.variable_pool_initializer import add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.entities.pause_reason import HumanInputRequired -from graphon.graph import Graph -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import ( - GraphEngineEvent, - GraphRunAbortedEvent, - GraphRunFailedEvent, - GraphRunPartialSucceededEvent, - GraphRunPausedEvent, - GraphRunStartedEvent, - GraphRunSucceededEvent, - NodeRunAgentLogEvent, - NodeRunExceptionEvent, - NodeRunFailedEvent, - NodeRunHumanInputFormFilledEvent, - NodeRunHumanInputFormTimeoutEvent, - NodeRunIterationFailedEvent, - NodeRunIterationNextEvent, - NodeRunIterationStartedEvent, - NodeRunIterationSucceededEvent, - NodeRunLoopFailedEvent, - NodeRunLoopNextEvent, - NodeRunLoopStartedEvent, - NodeRunLoopSucceededEvent, - NodeRunRetrieverResourceEvent, - NodeRunRetryEvent, - NodeRunStartedEvent, - NodeRunStreamChunkEvent, - NodeRunSucceededEvent, -) -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from models.workflow import Workflow from tasks.mail_human_input_delivery_task import dispatch_human_input_email_task diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py index 09992f4bbf..a3fb7b4c5d 100644 --- a/api/core/app/entities/app_invoke_entities.py +++ b/api/core/app/entities/app_invoke_entities.py @@ -2,13 +2,13 @@ from collections.abc import Mapping, Sequence from enum import StrEnum from typing import TYPE_CHECKING, Any +from graphon.file import File, FileUploadConfig +from graphon.model_runtime.entities.model_entities import AIModelEntity from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator from constants import UUID_NIL from core.app.app_config.entities import EasyUIBasedAppConfig, WorkflowUIBasedAppConfig from core.entities.provider_configuration import ProviderModelBundle -from graphon.file import File, FileUploadConfig -from graphon.model_runtime.entities.model_entities import AIModelEntity if TYPE_CHECKING: from core.ops.ops_trace_manager import TraceQueueManager diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index 221b7fb058..482f995d8e 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -3,14 +3,14 @@ from datetime import datetime from enum import StrEnum, auto from typing import Any -from pydantic import BaseModel, ConfigDict, Field - -from core.app.entities.agent_strategy import AgentStrategyInfo -from core.rag.entities import RetrievalSourceMetadata from graphon.entities import WorkflowStartReason from graphon.entities.pause_reason import PauseReason from graphon.enums import NodeType, WorkflowNodeExecutionMetadataKey from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk +from pydantic import BaseModel, ConfigDict, Field + +from core.app.entities.agent_strategy import AgentStrategyInfo +from core.rag.entities import RetrievalSourceMetadata class QueueEvent(StrEnum): diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index 6e4ca69cf0..88faf235d1 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -2,14 +2,14 @@ from collections.abc import Mapping, Sequence from enum import StrEnum from typing import Any -from pydantic import BaseModel, ConfigDict, Field - -from core.app.entities.agent_strategy import AgentStrategyInfo -from core.rag.entities import RetrievalSourceMetadata from graphon.entities import WorkflowStartReason from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage from graphon.nodes.human_input.entities import FormInput, UserAction +from pydantic import BaseModel, ConfigDict, Field + +from core.app.entities.agent_strategy import AgentStrategyInfo +from core.rag.entities import RetrievalSourceMetadata class AnnotationReplyAccount(BaseModel): diff --git a/api/core/app/features/hosting_moderation/hosting_moderation.py b/api/core/app/features/hosting_moderation/hosting_moderation.py index d59f5125e3..d2d2fea4fb 100644 --- a/api/core/app/features/hosting_moderation/hosting_moderation.py +++ b/api/core/app/features/hosting_moderation/hosting_moderation.py @@ -1,8 +1,9 @@ import logging +from graphon.model_runtime.entities.message_entities import PromptMessage + from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity from core.helper import moderation -from graphon.model_runtime.entities.message_entities import PromptMessage logger = logging.getLogger(__name__) diff --git a/api/core/app/layers/pause_state_persist_layer.py b/api/core/app/layers/pause_state_persist_layer.py index 9811f9f830..c027f42788 100644 --- a/api/core/app/layers/pause_state_persist_layer.py +++ b/api/core/app/layers/pause_state_persist_layer.py @@ -1,14 +1,14 @@ from dataclasses import dataclass from typing import Annotated, Literal, Self +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import GraphEngineEvent, GraphRunPausedEvent from pydantic import BaseModel, Field from sqlalchemy import Engine from sqlalchemy.orm import Session, sessionmaker from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity from core.workflow.system_variables import SystemVariableKey, get_system_text -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import GraphEngineEvent, GraphRunPausedEvent from models.model import AppMode from repositories.api_workflow_run_repository import APIWorkflowRunRepository from repositories.factory import DifyAPIRepositoryFactory diff --git a/api/core/app/layers/timeslice_layer.py b/api/core/app/layers/timeslice_layer.py index bb9fc1b6fa..8c8daf8712 100644 --- a/api/core/app/layers/timeslice_layer.py +++ b/api/core/app/layers/timeslice_layer.py @@ -3,10 +3,10 @@ import uuid from typing import ClassVar from apscheduler.schedulers.background import BackgroundScheduler # type: ignore - from graphon.graph_engine.entities.commands import CommandType, GraphEngineCommand from graphon.graph_engine.layers import GraphEngineLayer from graphon.graph_events import GraphEngineEvent + from services.workflow.entities import WorkflowScheduleCFSPlanEntity from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand diff --git a/api/core/app/layers/trigger_post_layer.py b/api/core/app/layers/trigger_post_layer.py index b60fe82ffe..77c7bec67e 100644 --- a/api/core/app/layers/trigger_post_layer.py +++ b/api/core/app/layers/trigger_post_layer.py @@ -2,12 +2,12 @@ import logging from datetime import UTC, datetime from typing import Any, ClassVar +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent, GraphRunPausedEvent, GraphRunSucceededEvent from pydantic import TypeAdapter from core.db.session_factory import session_factory from core.workflow.system_variables import SystemVariableKey, get_system_text -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent, GraphRunPausedEvent, GraphRunSucceededEvent from models.enums import WorkflowTriggerStatus from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity diff --git a/api/core/app/llm/model_access.py b/api/core/app/llm/model_access.py index c49c4eb0ac..278d0cb30b 100644 --- a/api/core/app/llm/model_access.py +++ b/api/core/app/llm/model_access.py @@ -2,15 +2,16 @@ from __future__ import annotations from typing import Any +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.nodes.llm.entities import ModelConfig +from graphon.nodes.llm.exc import LLMModeRequiredError, ModelNotExistError +from graphon.nodes.llm.protocols import CredentialsProvider + from core.app.entities.app_invoke_entities import DifyRunContext, ModelConfigWithCredentialsEntity from core.errors.error import ProviderTokenNotInitError from core.model_manager import ModelInstance, ModelManager from core.plugin.impl.model_runtime_factory import create_plugin_provider_manager from core.provider_manager import ProviderManager -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.nodes.llm.entities import ModelConfig -from graphon.nodes.llm.exc import LLMModeRequiredError, ModelNotExistError -from graphon.nodes.llm.protocols import CredentialsProvider class DifyCredentialsProvider: diff --git a/api/core/app/llm/quota.py b/api/core/app/llm/quota.py index b6039e1e4e..0bb10190c4 100644 --- a/api/core/app/llm/quota.py +++ b/api/core/app/llm/quota.py @@ -1,3 +1,4 @@ +from graphon.model_runtime.entities.llm_entities import LLMUsage from sqlalchemy import update from sqlalchemy.orm import sessionmaker @@ -7,7 +8,6 @@ from core.entities.provider_entities import ProviderQuotaType, QuotaUnit from core.errors.error import QuotaExceededError from core.model_manager import ModelInstance from extensions.ext_database import db -from graphon.model_runtime.entities.llm_entities import LLMUsage from libs.datetime_utils import naive_utc_now from models.provider import Provider, ProviderType from models.provider_ids import ModelProviderID diff --git a/api/core/app/task_pipeline/based_generate_task_pipeline.py b/api/core/app/task_pipeline/based_generate_task_pipeline.py index 9e688589db..10b9c36d3e 100644 --- a/api/core/app/task_pipeline/based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/based_generate_task_pipeline.py @@ -1,6 +1,7 @@ import logging import time +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from sqlalchemy import select from sqlalchemy.orm import Session @@ -17,7 +18,6 @@ from core.app.entities.task_entities import ( ) from core.errors.error import QuotaExceededError from core.moderation.output_moderation import ModerationRule, OutputModeration -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from models.enums import MessageStatus from models.model import Message diff --git a/api/core/app/workflow/layers/llm_quota.py b/api/core/app/workflow/layers/llm_quota.py index 4a7918032e..c577ce0754 100644 --- a/api/core/app/workflow/layers/llm_quota.py +++ b/api/core/app/workflow/layers/llm_quota.py @@ -7,16 +7,17 @@ This layer centralizes model-quota deduction outside node implementations. import logging from typing import TYPE_CHECKING, cast, final, override -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.app.llm import deduct_llm_quota, ensure_llm_quota_available -from core.errors.error import QuotaExceededError -from core.model_manager import ModelInstance from graphon.enums import BuiltinNodeTypes from graphon.graph_engine.entities.commands import AbortCommand, CommandType from graphon.graph_engine.layers import GraphEngineLayer from graphon.graph_events import GraphEngineEvent, GraphNodeEventBase, NodeRunSucceededEvent from graphon.nodes.base.node import Node +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.app.llm import deduct_llm_quota, ensure_llm_quota_available +from core.errors.error import QuotaExceededError +from core.model_manager import ModelInstance + if TYPE_CHECKING: from graphon.nodes.llm.node import LLMNode from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode diff --git a/api/core/app/workflow/layers/persistence.py b/api/core/app/workflow/layers/persistence.py index d521304615..602940af6e 100644 --- a/api/core/app/workflow/layers/persistence.py +++ b/api/core/app/workflow/layers/persistence.py @@ -14,13 +14,6 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Union -from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity -from core.ops.entities.trace_entity import TraceTaskName -from core.ops.ops_trace_manager import TraceQueueManager, TraceTask -from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository -from core.workflow.system_variables import SystemVariableKey -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID -from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run from graphon.entities import WorkflowExecution, WorkflowNodeExecution from graphon.enums import ( WorkflowExecutionStatus, @@ -45,6 +38,14 @@ from graphon.graph_events import ( NodeRunSucceededEvent, ) from graphon.node_events import NodeRunResult + +from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity +from core.ops.entities.trace_entity import TraceTaskName +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask +from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository +from core.workflow.system_variables import SystemVariableKey +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID +from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run from libs.datetime_utils import naive_utc_now diff --git a/api/core/base/tts/app_generator_tts_publisher.py b/api/core/base/tts/app_generator_tts_publisher.py index 9e3c187210..3d8a7a54f3 100644 --- a/api/core/base/tts/app_generator_tts_publisher.py +++ b/api/core/base/tts/app_generator_tts_publisher.py @@ -6,6 +6,9 @@ import re import threading from collections.abc import Iterable +from graphon.model_runtime.entities.message_entities import TextPromptMessageContent +from graphon.model_runtime.entities.model_entities import ModelType + from core.app.entities.queue_entities import ( MessageQueueMessage, QueueAgentMessageEvent, @@ -15,8 +18,6 @@ from core.app.entities.queue_entities import ( WorkflowQueueMessage, ) from core.model_manager import ModelInstance, ModelManager -from graphon.model_runtime.entities.message_entities import TextPromptMessageContent -from graphon.model_runtime.entities.model_entities import ModelType class AudioTrunk: diff --git a/api/core/datasource/datasource_manager.py b/api/core/datasource/datasource_manager.py index f0dcb13b62..4cb1905688 100644 --- a/api/core/datasource/datasource_manager.py +++ b/api/core/datasource/datasource_manager.py @@ -3,6 +3,9 @@ from collections.abc import Generator from threading import Lock from typing import Any, cast +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.file import File, FileTransferMethod, FileType, get_file_type_by_mime_type +from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent from sqlalchemy import select import contexts @@ -28,9 +31,6 @@ from core.plugin.impl.datasource import PluginDatasourceManager from core.workflow.file_reference import build_file_reference from core.workflow.nodes.datasource.entities import DatasourceParameter, OnlineDriveDownloadFileParam from factories import file_factory -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.file import File, FileTransferMethod, FileType, get_file_type_by_mime_type -from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent from models.model import UploadFile from models.tools import ToolFile from services.datasource_provider_service import DatasourceProviderService diff --git a/api/core/datasource/entities/api_entities.py b/api/core/datasource/entities/api_entities.py index 352e6bfd49..9c22d5e67c 100644 --- a/api/core/datasource/entities/api_entities.py +++ b/api/core/datasource/entities/api_entities.py @@ -1,10 +1,10 @@ from typing import Any, Literal, TypedDict +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from core.datasource.entities.datasource_entities import DatasourceParameter from core.tools.entities.common_entities import I18nObject, I18nObjectDict -from graphon.model_runtime.utils.encoders import jsonable_encoder class DatasourceApiEntity(BaseModel): diff --git a/api/core/datasource/utils/message_transformer.py b/api/core/datasource/utils/message_transformer.py index 6a3f9e684a..c012e128f4 100644 --- a/api/core/datasource/utils/message_transformer.py +++ b/api/core/datasource/utils/message_transformer.py @@ -2,10 +2,11 @@ import logging from collections.abc import Generator from mimetypes import guess_extension, guess_type +from graphon.file import File, FileTransferMethod, FileType + from core.datasource.entities.datasource_entities import DatasourceMessage from core.tools.tool_file_manager import ToolFileManager from core.workflow.file_reference import parse_file_reference -from graphon.file import File, FileTransferMethod, FileType from models.tools import ToolFile logger = logging.getLogger(__name__) diff --git a/api/core/entities/execution_extra_content.py b/api/core/entities/execution_extra_content.py index 04ae193396..d304c982cd 100644 --- a/api/core/entities/execution_extra_content.py +++ b/api/core/entities/execution_extra_content.py @@ -3,9 +3,9 @@ from __future__ import annotations from collections.abc import Mapping, Sequence from typing import Any, TypeAlias +from graphon.nodes.human_input.entities import FormInput, UserAction from pydantic import BaseModel, ConfigDict, Field -from graphon.nodes.human_input.entities import FormInput, UserAction from models.execution_extra_content import ExecutionContentType diff --git a/api/core/entities/mcp_provider.py b/api/core/entities/mcp_provider.py index bfa4f56915..a440829b46 100644 --- a/api/core/entities/mcp_provider.py +++ b/api/core/entities/mcp_provider.py @@ -6,6 +6,7 @@ from enum import StrEnum from typing import TYPE_CHECKING, Any from urllib.parse import urlparse +from graphon.file import helpers as file_helpers from pydantic import BaseModel from configs import dify_config @@ -15,7 +16,6 @@ from core.helper.provider_cache import NoOpProviderCredentialCache from core.mcp.types import OAuthClientInformation, OAuthClientMetadata, OAuthTokens from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType -from graphon.file import helpers as file_helpers if TYPE_CHECKING: from models.tools import MCPToolProvider diff --git a/api/core/entities/model_entities.py b/api/core/entities/model_entities.py index e99a131500..84d95c38c6 100644 --- a/api/core/entities/model_entities.py +++ b/api/core/entities/model_entities.py @@ -1,11 +1,10 @@ from collections.abc import Sequence from enum import StrEnum, auto -from pydantic import BaseModel, ConfigDict - from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import ModelType, ProviderModel from graphon.model_runtime.entities.provider_entities import ProviderEntity +from pydantic import BaseModel, ConfigDict class ModelStatus(StrEnum): diff --git a/api/core/entities/provider_entities.py b/api/core/entities/provider_entities.py index 72b29c2277..95431c0e01 100644 --- a/api/core/entities/provider_entities.py +++ b/api/core/entities/provider_entities.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import StrEnum, auto from typing import Any, Union +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, ConfigDict, Field from core.entities.parameter_entities import ( @@ -12,7 +13,6 @@ from core.entities.parameter_entities import ( ToolSelectorScope, ) from core.tools.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import ModelType class ProviderQuotaType(StrEnum): diff --git a/api/core/helper/code_executor/code_executor.py b/api/core/helper/code_executor/code_executor.py index 951e065b2c..35bfcfb6a5 100644 --- a/api/core/helper/code_executor/code_executor.py +++ b/api/core/helper/code_executor/code_executor.py @@ -4,6 +4,7 @@ from threading import Lock from typing import Any import httpx +from graphon.nodes.code.entities import CodeLanguage from pydantic import BaseModel from yarl import URL @@ -13,7 +14,6 @@ from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTr from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer from core.helper.code_executor.template_transformer import TemplateTransformer from core.helper.http_client_pooling import get_pooled_http_client -from graphon.nodes.code.entities import CodeLanguage logger = logging.getLogger(__name__) code_execution_endpoint_url = URL(str(dify_config.CODE_EXECUTION_ENDPOINT)) diff --git a/api/core/hosting_configuration.py b/api/core/hosting_configuration.py index 8bcb899b23..f8f56e12d2 100644 --- a/api/core/hosting_configuration.py +++ b/api/core/hosting_configuration.py @@ -1,12 +1,12 @@ from typing import Any from flask import Flask +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel from configs import dify_config from core.entities import DEFAULT_PLUGIN_ID from core.entities.provider_entities import ProviderQuotaType, QuotaUnit, RestrictModel -from graphon.model_runtime.entities.model_entities import ModelType class HostingQuota(BaseModel): diff --git a/api/core/llm_generator/output_parser/structured_output.py b/api/core/llm_generator/output_parser/structured_output.py index d2e375626f..a8ad7c9179 100644 --- a/api/core/llm_generator/output_parser/structured_output.py +++ b/api/core/llm_generator/output_parser/structured_output.py @@ -5,11 +5,6 @@ from enum import StrEnum from typing import Any, Literal, cast, overload import json_repair -from pydantic import TypeAdapter, ValidationError - -from core.llm_generator.output_parser.errors import OutputParserError -from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT -from core.model_manager import ModelInstance from graphon.model_runtime.callbacks.base_callback import Callback from graphon.model_runtime.entities.llm_entities import ( LLMResult, @@ -26,6 +21,11 @@ from graphon.model_runtime.entities.message_entities import ( TextPromptMessageContent, ) from graphon.model_runtime.entities.model_entities import AIModelEntity, ParameterRule +from pydantic import TypeAdapter, ValidationError + +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT +from core.model_manager import ModelInstance class ResponseFormat(StrEnum): diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 884610ca82..72171d1536 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -3,11 +3,12 @@ import logging from collections.abc import Mapping from typing import Any, NotRequired, TypedDict, cast +from graphon.variables.input_entities import VariableEntity, VariableEntityType + from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.mcp import types as mcp_types -from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import App, AppMCPServer, AppMode, EndUser from services.app_generate_service import AppGenerateService diff --git a/api/core/mcp/utils.py b/api/core/mcp/utils.py index 7b5a7635f1..7e35044176 100644 --- a/api/core/mcp/utils.py +++ b/api/core/mcp/utils.py @@ -4,11 +4,11 @@ from contextlib import AbstractContextManager import httpx import httpx_sse +from graphon.model_runtime.utils.encoders import jsonable_encoder from httpx_sse import connect_sse from configs import dify_config from core.mcp.types import ErrorData, JSONRPCError -from graphon.model_runtime.utils.encoders import jsonable_encoder HTTP_REQUEST_NODE_SSL_VERIFY = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index d840ee213c..5809d6f74a 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -1,14 +1,5 @@ from collections.abc import Sequence -from sqlalchemy import select -from sqlalchemy.orm import sessionmaker - -from core.app.app_config.features.file_upload.manager import FileUploadConfigManager -from core.app.file_access import DatabaseFileAccessController -from core.model_manager import ModelInstance -from core.prompt.utils.extract_thread_messages import extract_thread_messages -from extensions.ext_database import db -from factories import file_factory from graphon.file import file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -19,6 +10,15 @@ from graphon.model_runtime.entities import ( UserPromptMessage, ) from graphon.model_runtime.entities.message_entities import PromptMessageContentUnionTypes +from sqlalchemy import select +from sqlalchemy.orm import sessionmaker + +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.file_access import DatabaseFileAccessController +from core.model_manager import ModelInstance +from core.prompt.utils.extract_thread_messages import extract_thread_messages +from extensions.ext_database import db +from factories import file_factory from models.model import AppMode, Conversation, Message, MessageFile from models.workflow import Workflow from repositories.api_workflow_run_repository import APIWorkflowRunRepository diff --git a/api/core/plugin/backwards_invocation/model.py b/api/core/plugin/backwards_invocation/model.py index c92438960a..a4b24ff849 100644 --- a/api/core/plugin/backwards_invocation/model.py +++ b/api/core/plugin/backwards_invocation/model.py @@ -3,6 +3,20 @@ from binascii import hexlify, unhexlify from collections.abc import Generator from typing import Any +from graphon.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMResultChunkWithStructuredOutput, + LLMResultWithStructuredOutput, +) +from graphon.model_runtime.entities.message_entities import ( + PromptMessage, + SystemPromptMessage, + UserPromptMessage, +) +from graphon.model_runtime.entities.model_entities import ModelType + from core.app.llm import deduct_llm_quota from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output from core.model_manager import ModelManager @@ -19,19 +33,6 @@ from core.plugin.entities.request import ( ) from core.tools.entities.tool_entities import ToolProviderType from core.tools.utils.model_invocation_utils import ModelInvocationUtils -from graphon.model_runtime.entities.llm_entities import ( - LLMResult, - LLMResultChunk, - LLMResultChunkDelta, - LLMResultChunkWithStructuredOutput, - LLMResultWithStructuredOutput, -) -from graphon.model_runtime.entities.message_entities import ( - PromptMessage, - SystemPromptMessage, - UserPromptMessage, -) -from graphon.model_runtime.entities.model_entities import ModelType from models.account import Tenant diff --git a/api/core/plugin/backwards_invocation/node.py b/api/core/plugin/backwards_invocation/node.py index 9550e49992..9478997494 100644 --- a/api/core/plugin/backwards_invocation/node.py +++ b/api/core/plugin/backwards_invocation/node.py @@ -1,4 +1,3 @@ -from core.plugin.backwards_invocation.base import BaseBackwardsInvocation from graphon.enums import BuiltinNodeTypes from graphon.nodes.llm.entities import ModelConfig as LLMModelConfig from graphon.nodes.parameter_extractor.entities import ( @@ -9,6 +8,8 @@ from graphon.nodes.question_classifier.entities import ( ClassConfig, QuestionClassifierNodeData, ) + +from core.plugin.backwards_invocation.base import BaseBackwardsInvocation from services.workflow_service import WorkflowService diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index 89e0e8881c..4d28032a57 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -3,6 +3,7 @@ from collections.abc import Mapping from enum import StrEnum, auto from typing import Any +from graphon.model_runtime.entities.provider_entities import ProviderEntity from packaging.version import InvalidVersion, Version from pydantic import BaseModel, Field, field_validator, model_validator @@ -13,7 +14,6 @@ from core.plugin.entities.endpoint import EndpointProviderDeclaration from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntity from core.trigger.entities.entities import TriggerProviderEntity -from graphon.model_runtime.entities.provider_entities import ProviderEntity class PluginInstallationSource(StrEnum): diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index 257638ad77..e0ddb746c7 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -6,6 +6,8 @@ from datetime import datetime from enum import StrEnum from typing import Any +from graphon.model_runtime.entities.model_entities import AIModelEntity +from graphon.model_runtime.entities.provider_entities import ProviderEntity from pydantic import BaseModel, ConfigDict, Field from core.agent.plugin_entities import AgentProviderEntityWithPlugin @@ -16,8 +18,6 @@ from core.plugin.entities.plugin import PluginDeclaration, PluginEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin from core.trigger.entities.entities import TriggerProviderEntity -from graphon.model_runtime.entities.model_entities import AIModelEntity -from graphon.model_runtime.entities.provider_entities import ProviderEntity class PluginDaemonBasicResponse[T: BaseModel | dict | list | bool | str](BaseModel): diff --git a/api/core/plugin/entities/request.py b/api/core/plugin/entities/request.py index 1474883204..4a85952dcd 100644 --- a/api/core/plugin/entities/request.py +++ b/api/core/plugin/entities/request.py @@ -4,10 +4,6 @@ from collections.abc import Mapping from typing import Any, Literal from flask import Response -from pydantic import BaseModel, ConfigDict, Field, field_validator - -from core.entities.provider_entities import BasicProviderConfig -from core.plugin.utils.http_parser import deserialize_response from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, PromptMessage, @@ -25,6 +21,10 @@ from graphon.nodes.parameter_extractor.entities import ( from graphon.nodes.question_classifier.entities import ( ClassConfig, ) +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from core.entities.provider_entities import BasicProviderConfig +from core.plugin.utils.http_parser import deserialize_response class InvokeCredentials(BaseModel): diff --git a/api/core/plugin/impl/base.py b/api/core/plugin/impl/base.py index 9ee8469892..7f36560b49 100644 --- a/api/core/plugin/impl/base.py +++ b/api/core/plugin/impl/base.py @@ -5,6 +5,14 @@ from collections.abc import Callable, Generator from typing import Any, cast import httpx +from graphon.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic import BaseModel from yarl import URL @@ -29,14 +37,6 @@ from core.trigger.errors import ( TriggerPluginInvokeError, TriggerProviderCredentialValidationError, ) -from graphon.model_runtime.errors.invoke import ( - InvokeAuthorizationError, - InvokeBadRequestError, - InvokeConnectionError, - InvokeRateLimitError, - InvokeServerUnavailableError, -) -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError plugin_daemon_inner_api_baseurl = URL(str(dify_config.PLUGIN_DAEMON_URL)) _plugin_daemon_timeout_config = cast( diff --git a/api/core/plugin/impl/model.py b/api/core/plugin/impl/model.py index 47608bdfa6..703af63f7c 100644 --- a/api/core/plugin/impl/model.py +++ b/api/core/plugin/impl/model.py @@ -2,6 +2,13 @@ import binascii from collections.abc import Generator, Sequence from typing import IO, Any +from graphon.model_runtime.entities.llm_entities import LLMResultChunk +from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool +from graphon.model_runtime.entities.model_entities import AIModelEntity +from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult +from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult +from graphon.model_runtime.utils.encoders import jsonable_encoder + from core.plugin.entities.plugin_daemon import ( PluginBasicBooleanResponse, PluginDaemonInnerError, @@ -13,12 +20,6 @@ from core.plugin.entities.plugin_daemon import ( PluginVoicesResponse, ) from core.plugin.impl.base import BasePluginClient -from graphon.model_runtime.entities.llm_entities import LLMResultChunk -from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool -from graphon.model_runtime.entities.model_entities import AIModelEntity -from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult -from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult -from graphon.model_runtime.utils.encoders import jsonable_encoder class PluginModelClient(BasePluginClient): diff --git a/api/core/plugin/utils/converter.py b/api/core/plugin/utils/converter.py index 12d8e282b2..90350f8400 100644 --- a/api/core/plugin/utils/converter.py +++ b/api/core/plugin/utils/converter.py @@ -1,8 +1,9 @@ from typing import Any -from core.tools.entities.tool_entities import ToolSelector from graphon.file import File +from core.tools.entities.tool_entities import ToolSelector + def convert_parameters_to_plugin_format(parameters: dict[str, Any]) -> dict[str, Any]: for parameter_name, parameter in parameters.items(): diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py index 24e05ef865..19b5e9223a 100644 --- a/api/core/prompt/advanced_prompt_transform.py +++ b/api/core/prompt/advanced_prompt_transform.py @@ -1,13 +1,6 @@ from collections.abc import Mapping, Sequence from typing import cast -from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity -from core.helper.code_executor.jinja2.jinja2_formatter import Jinja2Formatter -from core.memory.token_buffer_memory import TokenBufferMemory -from core.model_manager import ModelInstance -from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig -from core.prompt.prompt_transform import PromptTransform -from core.prompt.utils.prompt_template_parser import PromptTemplateParser from graphon.file import File, file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -20,6 +13,14 @@ from graphon.model_runtime.entities import ( from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes from graphon.runtime import VariablePool +from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.helper.code_executor.jinja2.jinja2_formatter import Jinja2Formatter +from core.memory.token_buffer_memory import TokenBufferMemory +from core.model_manager import ModelInstance +from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig +from core.prompt.prompt_transform import PromptTransform +from core.prompt.utils.prompt_template_parser import PromptTemplateParser + class AdvancedPromptTransform(PromptTransform): """ diff --git a/api/core/prompt/agent_history_prompt_transform.py b/api/core/prompt/agent_history_prompt_transform.py index 7c6280fe93..126888f739 100644 --- a/api/core/prompt/agent_history_prompt_transform.py +++ b/api/core/prompt/agent_history_prompt_transform.py @@ -1,10 +1,5 @@ from typing import cast -from core.app.entities.app_invoke_entities import ( - ModelConfigWithCredentialsEntity, -) -from core.memory.token_buffer_memory import TokenBufferMemory -from core.prompt.prompt_transform import PromptTransform from graphon.model_runtime.entities.message_entities import ( PromptMessage, SystemPromptMessage, @@ -12,6 +7,12 @@ from graphon.model_runtime.entities.message_entities import ( ) from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel +from core.app.entities.app_invoke_entities import ( + ModelConfigWithCredentialsEntity, +) +from core.memory.token_buffer_memory import TokenBufferMemory +from core.prompt.prompt_transform import PromptTransform + class AgentHistoryPromptTransform(PromptTransform): """ diff --git a/api/core/prompt/prompt_transform.py b/api/core/prompt/prompt_transform.py index 6ff2f44cdc..4539ae9f11 100644 --- a/api/core/prompt/prompt_transform.py +++ b/api/core/prompt/prompt_transform.py @@ -1,11 +1,12 @@ from typing import Any +from graphon.model_runtime.entities.message_entities import PromptMessage +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelPropertyKey + from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance from core.prompt.entities.advanced_prompt_entities import MemoryConfig -from graphon.model_runtime.entities.message_entities import PromptMessage -from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelPropertyKey class PromptTransform: diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py index 1665bdeb52..dc8391a6a5 100644 --- a/api/core/prompt/simple_prompt_transform.py +++ b/api/core/prompt/simple_prompt_transform.py @@ -4,12 +4,6 @@ from collections.abc import Mapping, Sequence from enum import StrEnum, auto from typing import TYPE_CHECKING, Any, TypedDict, cast -from core.app.app_config.entities import PromptTemplateEntity -from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity -from core.memory.token_buffer_memory import TokenBufferMemory -from core.prompt.entities.advanced_prompt_entities import MemoryConfig -from core.prompt.prompt_transform import PromptTransform -from core.prompt.utils.prompt_template_parser import PromptTemplateParser from graphon.file import file_manager from graphon.model_runtime.entities.message_entities import ( ImagePromptMessageContent, @@ -19,6 +13,13 @@ from graphon.model_runtime.entities.message_entities import ( TextPromptMessageContent, UserPromptMessage, ) + +from core.app.app_config.entities import PromptTemplateEntity +from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.memory.token_buffer_memory import TokenBufferMemory +from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from core.prompt.prompt_transform import PromptTransform +from core.prompt.utils.prompt_template_parser import PromptTemplateParser from models.model import AppMode if TYPE_CHECKING: diff --git a/api/core/prompt/utils/prompt_message_util.py b/api/core/prompt/utils/prompt_message_util.py index ba76eb0c4e..dbda749925 100644 --- a/api/core/prompt/utils/prompt_message_util.py +++ b/api/core/prompt/utils/prompt_message_util.py @@ -1,7 +1,6 @@ from collections.abc import Sequence from typing import Any, cast -from core.prompt.simple_prompt_transform import ModelMode from graphon.model_runtime.entities import ( AssistantPromptMessage, AudioPromptMessageContent, @@ -12,6 +11,8 @@ from graphon.model_runtime.entities import ( TextPromptMessageContent, ) +from core.prompt.simple_prompt_transform import ModelMode + class PromptMessageUtil: @staticmethod diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index c3bbe8fc09..39ef31632e 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -6,6 +6,14 @@ from collections.abc import Sequence from json import JSONDecodeError from typing import TYPE_CHECKING, Any +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + CredentialFormSchema, + FormType, + ProviderEntity, +) +from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.exc import IntegrityError @@ -33,14 +41,6 @@ from core.helper.position_helper import is_filtered from extensions import ext_hosting_provider from extensions.ext_database import db from extensions.ext_redis import redis_client -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - CredentialFormSchema, - FormType, - ProviderEntity, -) -from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from models.provider import ( LoadBalancingModelConfig, Provider, diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index 7e71d67ec0..f978e072f3 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -4,6 +4,7 @@ from concurrent.futures import ThreadPoolExecutor from typing import Any, NotRequired, TypedDict from flask import Flask, current_app +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import select from sqlalchemy.orm import Session, load_only @@ -23,7 +24,6 @@ from core.rag.rerank.rerank_type import RerankMode from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.tools.signature import sign_upload_file from extensions.ext_database import db -from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import ( ChildChunk, Dataset, @@ -195,23 +195,6 @@ class RetrievalService: ) return all_documents - @classmethod - def _filter_documents_by_vector_score_threshold( - cls, documents: list[Document], score_threshold: float | None - ) -> list[Document]: - """Keep documents whose stored retrieval score meets the threshold. - - Used when hybrid search skips early vector thresholding but no rerank - runner applies a threshold afterward (same rule as ``calculate_vector_score``). - """ - if score_threshold is None: - return documents - return [ - document - for document in documents - if document.metadata and document.metadata.get("score", 0) >= score_threshold - ] - @classmethod def _deduplicate_documents(cls, documents: list[Document]) -> list[Document]: """Deduplicate documents in O(n) while preserving first-seen order. @@ -311,20 +294,13 @@ class RetrievalService: vector = Vector(dataset=dataset) documents = [] - # Hybrid search merges keyword / full-text / vector hits and then reranks - # (weighted fusion or reranking model). Applying the user score threshold at - # vector retrieval time uses embedding similarity, which is not comparable to - # reranked or fused scores and incorrectly drops high-quality chunks (#35233). - embedding_score_threshold = ( - 0.0 if retrieval_method == RetrievalMethod.HYBRID_SEARCH else score_threshold - ) if query_type == QueryType.TEXT_QUERY: documents.extend( vector.search_by_vector( query, search_type="similarity_score_threshold", top_k=top_k, - score_threshold=embedding_score_threshold, + score_threshold=score_threshold, filter={"group_id": [dataset.id]}, document_ids_filter=document_ids_filter, ) @@ -336,7 +312,7 @@ class RetrievalService: vector.search_by_file( file_id=query, top_k=top_k, - score_threshold=embedding_score_threshold, + score_threshold=score_threshold, filter={"group_id": [dataset.id]}, document_ids_filter=document_ids_filter, ) @@ -868,10 +844,6 @@ class RetrievalService: top_n=top_k, query_type=QueryType.TEXT_QUERY if query else QueryType.IMAGE_QUERY, ) - if not data_post_processor.rerank_runner and score_threshold: - all_documents_item = self._filter_documents_by_vector_score_threshold( - all_documents_item, score_threshold - ) all_documents.extend(all_documents_item) diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index 59d7f3c3c4..dddd5fc994 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -4,6 +4,7 @@ import time from abc import ABC, abstractmethod from typing import Any +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import select from configs import dify_config @@ -18,7 +19,6 @@ from core.rag.models.document import Document from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.ext_storage import storage -from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, Whitelist from models.model import UploadFile diff --git a/api/core/rag/docstore/dataset_docstore.py b/api/core/rag/docstore/dataset_docstore.py index f4699f6869..8e9ebdd17a 100644 --- a/api/core/rag/docstore/dataset_docstore.py +++ b/api/core/rag/docstore/dataset_docstore.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Sequence from typing import Any +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import delete, func, select from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.models.document import AttachmentDocument, Document from extensions.ext_database import db -from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import ChildChunk, Dataset, DocumentSegment, SegmentAttachmentBinding diff --git a/api/core/rag/index_processor/processor/paragraph_index_processor.py b/api/core/rag/index_processor/processor/paragraph_index_processor.py index 7ffa9afafd..ace0b791e8 100644 --- a/api/core/rag/index_processor/processor/paragraph_index_processor.py +++ b/api/core/rag/index_processor/processor/paragraph_index_processor.py @@ -7,6 +7,16 @@ from typing import Any, TypedDict, cast logger = logging.getLogger(__name__) +from graphon.file import File, FileTransferMethod, FileType, file_manager +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + ImagePromptMessageContent, + PromptMessage, + PromptMessageContentUnionTypes, + TextPromptMessageContent, + UserPromptMessage, +) +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from sqlalchemy import select from core.app.file_access import DatabaseFileAccessController @@ -33,16 +43,6 @@ from core.tools.utils.text_processing_utils import remove_leading_symbols from core.workflow.file_reference import build_file_reference from extensions.ext_database import db from factories.file_factory import build_from_mapping -from graphon.file import File, FileTransferMethod, FileType, file_manager -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - ImagePromptMessageContent, - PromptMessage, - PromptMessageContentUnionTypes, - TextPromptMessageContent, - UserPromptMessage, -) -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from libs import helper from models import UploadFile from models.account import Account diff --git a/api/core/rag/models/document.py b/api/core/rag/models/document.py index 4ebf095904..087736d0b0 100644 --- a/api/core/rag/models/document.py +++ b/api/core/rag/models/document.py @@ -2,9 +2,8 @@ from abc import ABC, abstractmethod from collections.abc import Sequence from typing import Any -from pydantic import BaseModel, Field - from graphon.file import File +from pydantic import BaseModel, Field class ChildDocument(BaseModel): diff --git a/api/core/rag/rerank/rerank_model.py b/api/core/rag/rerank/rerank_model.py index bce08f998f..a8d37845a5 100644 --- a/api/core/rag/rerank/rerank_model.py +++ b/api/core/rag/rerank/rerank_model.py @@ -1,5 +1,8 @@ import base64 +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult + from core.model_manager import ModelInstance, ModelManager from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.query_type import QueryType @@ -7,8 +10,6 @@ from core.rag.models.document import Document from core.rag.rerank.rerank_base import BaseRerankRunner from extensions.ext_database import db from extensions.ext_storage import storage -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult from models.model import UploadFile diff --git a/api/core/rag/rerank/weight_rerank.py b/api/core/rag/rerank/weight_rerank.py index d0732b269a..49123e13d0 100644 --- a/api/core/rag/rerank/weight_rerank.py +++ b/api/core/rag/rerank/weight_rerank.py @@ -2,6 +2,7 @@ import math from collections import Counter import numpy as np +from graphon.model_runtime.entities.model_entities import ModelType from core.model_manager import ModelManager from core.rag.datasource.keyword.jieba.jieba_keyword_table_handler import JiebaKeywordTableHandler @@ -11,7 +12,6 @@ from core.rag.index_processor.constant.query_type import QueryType from core.rag.models.document import Document from core.rag.rerank.entity.weight import VectorSetting, Weights from core.rag.rerank.rerank_base import BaseRerankRunner -from graphon.model_runtime.entities.model_entities import ModelType class WeightRerankRunner(BaseRerankRunner): diff --git a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py index e617a9660e..dce7b6226c 100644 --- a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py +++ b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py @@ -1,9 +1,10 @@ from typing import Union +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import PromptMessageTool, SystemPromptMessage, UserPromptMessage + from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.model_manager import ModelInstance -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import PromptMessageTool, SystemPromptMessage, UserPromptMessage class FunctionCallMultiDatasetRouter: diff --git a/api/core/repositories/celery_workflow_execution_repository.py b/api/core/repositories/celery_workflow_execution_repository.py index e87d1cd6b2..b07c63fdf0 100644 --- a/api/core/repositories/celery_workflow_execution_repository.py +++ b/api/core/repositories/celery_workflow_execution_repository.py @@ -7,11 +7,11 @@ providing improved performance by offloading database operations to background w import logging +from graphon.entities import WorkflowExecution from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.factory import WorkflowExecutionRepository -from graphon.entities import WorkflowExecution from libs.helper import extract_tenant_id from models import Account, CreatorUserRole, EndUser from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/repositories/celery_workflow_node_execution_repository.py b/api/core/repositories/celery_workflow_node_execution_repository.py index 2451563317..cdb3af01a8 100644 --- a/api/core/repositories/celery_workflow_node_execution_repository.py +++ b/api/core/repositories/celery_workflow_node_execution_repository.py @@ -8,6 +8,7 @@ providing improved performance by offloading database operations to background w import logging from collections.abc import Sequence +from graphon.entities import WorkflowNodeExecution from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker @@ -15,7 +16,6 @@ from core.repositories.factory import ( OrderConfig, WorkflowNodeExecutionRepository, ) -from graphon.entities import WorkflowNodeExecution from libs.helper import extract_tenant_id from models import Account, CreatorUserRole, EndUser from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/core/repositories/factory.py b/api/core/repositories/factory.py index 4e83e70799..ce3ad15759 100644 --- a/api/core/repositories/factory.py +++ b/api/core/repositories/factory.py @@ -9,11 +9,11 @@ from collections.abc import Sequence from dataclasses import dataclass from typing import Literal, Protocol +from graphon.entities import WorkflowExecution, WorkflowNodeExecution from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from configs import dify_config -from graphon.entities import WorkflowExecution, WorkflowNodeExecution from libs.module_loading import import_string from models import Account, EndUser from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/repositories/sqlalchemy_workflow_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_execution_repository.py index 6be3902317..d74cc8f231 100644 --- a/api/core/repositories/sqlalchemy_workflow_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_execution_repository.py @@ -5,13 +5,13 @@ SQLAlchemy implementation of the WorkflowExecutionRepository. import json import logging +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowExecutionStatus, WorkflowType +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.factory import WorkflowExecutionRepository -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowExecutionStatus, WorkflowType -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from models import ( Account, diff --git a/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py index b036687bc9..13e885672a 100644 --- a/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py @@ -10,6 +10,10 @@ from concurrent.futures import ThreadPoolExecutor from typing import Any import psycopg2.errors +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import UnaryExpression, asc, desc, select from sqlalchemy.engine import Engine from sqlalchemy.exc import IntegrityError @@ -19,10 +23,6 @@ from tenacity import before_sleep_log, retry, retry_if_exception, stop_after_att from configs import dify_config from core.repositories.factory import OrderConfig, WorkflowNodeExecutionRepository from extensions.ext_storage import storage -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from libs.uuid_utils import uuidv7 from models import ( diff --git a/api/core/tools/builtin_tool/providers/audio/tools/asr.py b/api/core/tools/builtin_tool/providers/audio/tools/asr.py index 95660ab93b..e539074303 100644 --- a/api/core/tools/builtin_tool/providers/audio/tools/asr.py +++ b/api/core/tools/builtin_tool/providers/audio/tools/asr.py @@ -2,14 +2,15 @@ import io from collections.abc import Generator from typing import Any +from graphon.file import FileType +from graphon.file.file_manager import download +from graphon.model_runtime.entities.model_entities import ModelType + from core.model_manager import ModelManager from core.plugin.entities.parameters import PluginParameterOption from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter -from graphon.file import FileType -from graphon.file.file_manager import download -from graphon.model_runtime.entities.model_entities import ModelType from services.model_provider_service import ModelProviderService diff --git a/api/core/tools/builtin_tool/providers/audio/tools/tts.py b/api/core/tools/builtin_tool/providers/audio/tools/tts.py index ac3820f1ab..f49c669fe0 100644 --- a/api/core/tools/builtin_tool/providers/audio/tools/tts.py +++ b/api/core/tools/builtin_tool/providers/audio/tools/tts.py @@ -2,12 +2,13 @@ import io from collections.abc import Generator from typing import Any +from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType + from core.model_manager import ModelManager from core.plugin.entities.parameters import PluginParameterOption from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType from services.model_provider_service import ModelProviderService diff --git a/api/core/tools/builtin_tool/tool.py b/api/core/tools/builtin_tool/tool.py index d41503e1e6..14af63a962 100644 --- a/api/core/tools/builtin_tool/tool.py +++ b/api/core/tools/builtin_tool/tool.py @@ -1,11 +1,12 @@ from __future__ import annotations +from graphon.model_runtime.entities.llm_entities import LLMResult +from graphon.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage + from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ToolProviderType from core.tools.utils.model_invocation_utils import ModelInvocationUtils -from graphon.model_runtime.entities.llm_entities import LLMResult -from graphon.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage _SUMMARY_PROMPT = """You are a professional language researcher, you are interested in the language and you can quickly aimed at the main point of an webpage and reproduce it in your own words but diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 168e5f4493..0a2c37c563 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -6,6 +6,7 @@ from typing import Any, Union from urllib.parse import urlencode import httpx +from graphon.file.file_manager import download from core.helper import ssrf_proxy from core.tools.__base.tool import Tool @@ -13,7 +14,6 @@ from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_bundle import ApiToolBundle from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType from core.tools.errors import ToolInvokeError, ToolParameterValidationError, ToolProviderCredentialValidationError -from graphon.file.file_manager import download API_TOOL_DEFAULT_TIMEOUT = ( int(getenv("API_TOOL_DEFAULT_CONNECT_TIMEOUT", "10")), diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index 42a88c0003..410ec72baf 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from datetime import datetime from typing import Any, Literal +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from core.entities.mcp_provider import MCPAuthentication, MCPConfiguration @@ -9,7 +10,6 @@ from core.plugin.entities.plugin_daemon import CredentialType from core.tools.__base.tool import ToolParameter from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType -from graphon.model_runtime.utils.encoders import jsonable_encoder class ToolApiEntity(BaseModel): diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py index 00fc8a8282..f6d09472b3 100644 --- a/api/core/tools/mcp_tool/tool.py +++ b/api/core/tools/mcp_tool/tool.py @@ -6,6 +6,8 @@ import logging from collections.abc import Generator, Mapping from typing import Any, cast +from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata + from core.mcp.auth_client import MCPClientWithAuthRetry from core.mcp.error import MCPConnectionError from core.mcp.types import ( @@ -21,7 +23,6 @@ from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType from core.tools.errors import ToolInvokeError -from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata logger = logging.getLogger(__name__) diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 3caacb8706..d060fa8b49 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -7,6 +7,7 @@ from datetime import UTC, datetime from mimetypes import guess_type from typing import Any, Union, cast +from graphon.file import FileTransferMethod, FileType from yarl import URL from core.app.entities.app_invoke_entities import InvokeFrom @@ -32,7 +33,6 @@ from core.tools.errors import ( from core.tools.utils.message_transformer import ToolFileMessageTransformer, safe_json_value from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db -from graphon.file import FileTransferMethod, FileType from models.enums import CreatorUserRole, MessageFileBelongsTo from models.model import Message, MessageFile diff --git a/api/core/tools/tool_file_manager.py b/api/core/tools/tool_file_manager.py index c87e8a3ae0..ada3ed7f43 100644 --- a/api/core/tools/tool_file_manager.py +++ b/api/core/tools/tool_file_manager.py @@ -9,6 +9,7 @@ from mimetypes import guess_extension, guess_type from uuid import uuid4 import httpx +from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type from sqlalchemy import select from configs import dify_config @@ -16,7 +17,6 @@ from core.db.session_factory import session_factory from core.helper import ssrf_proxy from core.workflow.file_reference import build_file_reference from extensions.ext_storage import storage -from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type from models.model import MessageFile from models.tools import ToolFile diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 87cf6d7085..31964b6702 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -8,6 +8,7 @@ from threading import Lock from typing import TYPE_CHECKING, Any, Literal, Protocol, cast import sqlalchemy as sa +from graphon.runtime import VariablePool from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.orm import Session @@ -28,13 +29,14 @@ from core.tools.plugin_tool.tool import PluginTool from core.tools.utils.uuid_utils import is_valid_uuid from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from extensions.ext_database import db -from graphon.runtime import VariablePool from models.provider_ids import ToolProviderID from services.tools.mcp_tools_manage_service import MCPToolManageService if TYPE_CHECKING: pass +from graphon.model_runtime.utils.encoders import jsonable_encoder + from core.agent.entities import AgentToolEntity from core.app.entities.app_invoke_entities import InvokeFrom from core.helper.module_import_helper import load_single_subclass_from_source @@ -60,7 +62,6 @@ from core.tools.tool_label_manager import ToolLabelManager from core.tools.utils.configuration import ToolParameterConfigurationManager from core.tools.utils.encryption import create_provider_encrypter, create_tool_provider_encrypter from core.tools.workflow_as_tool.tool import WorkflowTool -from graphon.model_runtime.utils.encoders import jsonable_encoder from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider from services.tools.tools_transform_service import ToolTransformService diff --git a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py index b6890b2611..03e3c5918d 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py @@ -1,6 +1,7 @@ import threading from flask import Flask, current_app +from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field from sqlalchemy import select @@ -14,7 +15,6 @@ from core.rag.rerank.rerank_model import RerankModelRunner from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.tools.utils.dataset_retriever.dataset_retriever_base_tool import DatasetRetrieverBaseTool from extensions.ext_database import db -from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, Document, DocumentSegment default_retrieval_model: DefaultRetrievalModelDict = { diff --git a/api/core/tools/utils/message_transformer.py b/api/core/tools/utils/message_transformer.py index 79d0c114d4..81c85bc90d 100644 --- a/api/core/tools/utils/message_transformer.py +++ b/api/core/tools/utils/message_transformer.py @@ -9,11 +9,11 @@ from uuid import UUID import numpy as np import pytz +from graphon.file import File, FileTransferMethod, FileType from core.tools.entities.tool_entities import ToolInvokeMessage from core.tools.tool_file_manager import ToolFileManager from core.workflow.file_reference import parse_file_reference -from graphon.file import File, FileTransferMethod, FileType from libs.login import current_user from models import Account diff --git a/api/core/tools/utils/model_invocation_utils.py b/api/core/tools/utils/model_invocation_utils.py index a3623d4ecd..e534a9415a 100644 --- a/api/core/tools/utils/model_invocation_utils.py +++ b/api/core/tools/utils/model_invocation_utils.py @@ -8,9 +8,6 @@ import json from decimal import Decimal from typing import cast -from core.model_manager import ModelManager -from core.tools.entities.tool_entities import ToolProviderType -from extensions.ext_database import db from graphon.model_runtime.entities.llm_entities import LLMResult from graphon.model_runtime.entities.message_entities import PromptMessage from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType @@ -23,6 +20,10 @@ from graphon.model_runtime.errors.invoke import ( ) from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel from graphon.model_runtime.utils.encoders import jsonable_encoder + +from core.model_manager import ModelManager +from core.tools.entities.tool_entities import ToolProviderType +from extensions.ext_database import db from models.tools import ToolModelInvoke diff --git a/api/core/tools/utils/workflow_configuration_sync.py b/api/core/tools/utils/workflow_configuration_sync.py index 45718cadb6..2159eb8638 100644 --- a/api/core/tools/utils/workflow_configuration_sync.py +++ b/api/core/tools/utils/workflow_configuration_sync.py @@ -1,12 +1,13 @@ from collections.abc import Mapping, Sequence from typing import Any -from core.tools.entities.tool_entities import WorkflowToolParameterConfiguration -from core.tools.errors import WorkflowToolHumanInputNotSupportedError from graphon.enums import BuiltinNodeTypes from graphon.nodes.base.entities import OutputVariableEntity from graphon.variables.input_entities import VariableEntity +from core.tools.entities.tool_entities import WorkflowToolParameterConfiguration +from core.tools.errors import WorkflowToolHumanInputNotSupportedError + class WorkflowToolConfigurationUtils: @classmethod diff --git a/api/core/tools/workflow_as_tool/provider.py b/api/core/tools/workflow_as_tool/provider.py index 5905fd919e..a01004448a 100644 --- a/api/core/tools/workflow_as_tool/provider.py +++ b/api/core/tools/workflow_as_tool/provider.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from graphon.variables.input_entities import VariableEntity, VariableEntityType from pydantic import Field from sqlalchemy import select from sqlalchemy.orm import Session @@ -24,7 +25,6 @@ from core.tools.entities.tool_entities import ( from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurationUtils from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db -from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.account import Account from models.model import App, AppMode from models.tools import WorkflowToolProvider diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index cd8c6352b5..e510cfcead 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -5,6 +5,8 @@ import logging from collections.abc import Generator, Mapping, Sequence from typing import Any, cast +from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod +from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata from sqlalchemy import select from core.app.file_access import DatabaseFileAccessController @@ -20,8 +22,6 @@ from core.tools.entities.tool_entities import ( from core.tools.errors import ToolInvokeError from core.workflow.file_reference import resolve_file_record_id from factories.file_factory import build_from_mapping -from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod -from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata from models import Account, Tenant from models.model import App, EndUser from models.utils.file_input_compat import build_file_from_stored_mapping diff --git a/api/core/trigger/debug/event_selectors.py b/api/core/trigger/debug/event_selectors.py index 24c1271488..61d1cd8540 100644 --- a/api/core/trigger/debug/event_selectors.py +++ b/api/core/trigger/debug/event_selectors.py @@ -8,6 +8,7 @@ from collections.abc import Mapping from datetime import datetime from typing import Any +from graphon.entities.graph_config import NodeConfigDict from pydantic import BaseModel from core.plugin.entities.request import TriggerInvokeEventResponse @@ -27,7 +28,6 @@ from core.trigger.debug.events import ( from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData from core.workflow.nodes.trigger_schedule.entities import ScheduleConfig from extensions.ext_redis import redis_client -from graphon.entities.graph_config import NodeConfigDict from libs.datetime_utils import ensure_naive_utc, naive_utc_now from libs.schedule_utils import calculate_next_run_at from models.model import App diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py index 51452c29a3..c52aad150b 100644 --- a/api/core/workflow/nodes/agent/entities.py +++ b/api/core/workflow/nodes/agent/entities.py @@ -1,12 +1,12 @@ from enum import IntEnum, StrEnum, auto from typing import Any, Literal, Union +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import BuiltinNodeTypes, NodeType from pydantic import BaseModel from core.prompt.entities.advanced_prompt_entities import MemoryConfig from core.tools.entities.tool_entities import ToolSelector -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import BuiltinNodeTypes, NodeType class AgentNodeData(BaseNodeData): diff --git a/api/core/workflow/nodes/datasource/entities.py b/api/core/workflow/nodes/datasource/entities.py index 28966f2392..cad32f8d5b 100644 --- a/api/core/workflow/nodes/datasource/entities.py +++ b/api/core/workflow/nodes/datasource/entities.py @@ -1,10 +1,9 @@ from typing import Any, Literal, Union -from pydantic import BaseModel, field_validator -from pydantic_core.core_schema import ValidationInfo - from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType +from pydantic import BaseModel, field_validator +from pydantic_core.core_schema import ValidationInfo class DatasourceEntity(BaseModel): diff --git a/api/core/workflow/nodes/knowledge_index/entities.py b/api/core/workflow/nodes/knowledge_index/entities.py index 260881e49c..04a10f9257 100644 --- a/api/core/workflow/nodes/knowledge_index/entities.py +++ b/api/core/workflow/nodes/knowledge_index/entities.py @@ -1,13 +1,13 @@ from typing import Union +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType from pydantic import BaseModel from core.rag.entities import RerankingModelConfig, WeightedScoreConfig from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType class RetrievalSetting(BaseModel): diff --git a/api/core/workflow/nodes/knowledge_retrieval/entities.py b/api/core/workflow/nodes/knowledge_retrieval/entities.py index 3825f526a2..460ec693ce 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/entities.py +++ b/api/core/workflow/nodes/knowledge_retrieval/entities.py @@ -1,11 +1,11 @@ from typing import Literal -from pydantic import BaseModel, Field - -from core.rag.entities import Condition, MetadataFilteringCondition, RerankingModelConfig, WeightedScoreConfig from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.llm.entities import ModelConfig, VisionConfig +from pydantic import BaseModel, Field + +from core.rag.entities import Condition, MetadataFilteringCondition, RerankingModelConfig, WeightedScoreConfig __all__ = ["Condition"] diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py index 25f73e446d..6109ced8e2 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -8,11 +8,6 @@ import logging from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Literal -from core.app.app_config.entities import DatasetRetrieveConfigEntity -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval -from core.workflow.file_reference import parse_file_reference from graphon.entities import GraphInitParams from graphon.enums import ( BuiltinNodeTypes, @@ -31,6 +26,12 @@ from graphon.variables import ( ) from graphon.variables.segments import ArrayObjectSegment +from core.app.app_config.entities import DatasetRetrieveConfigEntity +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict +from core.rag.retrieval.dataset_retrieval import DatasetRetrieval +from core.workflow.file_reference import parse_file_reference + from .entities import ( Condition, KnowledgeRetrievalNodeData, diff --git a/api/core/workflow/nodes/trigger_plugin/entities.py b/api/core/workflow/nodes/trigger_plugin/entities.py index 23ed2cd408..bf5be2379a 100644 --- a/api/core/workflow/nodes/trigger_plugin/entities.py +++ b/api/core/workflow/nodes/trigger_plugin/entities.py @@ -1,12 +1,12 @@ from collections.abc import Mapping from typing import Any, Literal, Union +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType from pydantic import BaseModel, Field, ValidationInfo, field_validator from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE from core.trigger.entities.entities import EventParameter -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType from .exc import TriggerEventParameterError diff --git a/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py b/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py index c848a86255..e50de11bb9 100644 --- a/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py +++ b/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py @@ -1,12 +1,13 @@ from collections.abc import Mapping from typing import Any -from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID from graphon.enums import NodeExecutionType, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from graphon.node_events import NodeRunResult from graphon.nodes.base.node import Node +from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID + from .entities import TriggerEventNodeData diff --git a/api/core/workflow/nodes/trigger_schedule/entities.py b/api/core/workflow/nodes/trigger_schedule/entities.py index 683c8d420f..04f1f7e6bb 100644 --- a/api/core/workflow/nodes/trigger_schedule/entities.py +++ b/api/core/workflow/nodes/trigger_schedule/entities.py @@ -1,10 +1,10 @@ from typing import Any, Literal, Union +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType from pydantic import BaseModel, Field from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType class TriggerScheduleNodeData(BaseNodeData): diff --git a/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py b/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py index b46cc76a6e..a9753ab387 100644 --- a/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py +++ b/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py @@ -1,11 +1,12 @@ from collections.abc import Mapping -from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus from graphon.node_events import NodeRunResult from graphon.nodes.base.node import Node +from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID + from .entities import TriggerScheduleNodeData diff --git a/api/core/workflow/nodes/trigger_webhook/entities.py b/api/core/workflow/nodes/trigger_webhook/entities.py index b261039448..a30f877e4b 100644 --- a/api/core/workflow/nodes/trigger_webhook/entities.py +++ b/api/core/workflow/nodes/trigger_webhook/entities.py @@ -1,12 +1,12 @@ from collections.abc import Sequence from enum import StrEnum -from pydantic import BaseModel, Field, field_validator - -from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE from graphon.entities.base_node_data import BaseNodeData from graphon.enums import NodeType from graphon.variables.types import SegmentType +from pydantic import BaseModel, Field, field_validator + +from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE _WEBHOOK_HEADER_ALLOWED_TYPES: frozenset[SegmentType] = frozenset((SegmentType.STRING,)) diff --git a/api/core/workflow/nodes/trigger_webhook/node.py b/api/core/workflow/nodes/trigger_webhook/node.py index 13c4f05bfd..d942a718cc 100644 --- a/api/core/workflow/nodes/trigger_webhook/node.py +++ b/api/core/workflow/nodes/trigger_webhook/node.py @@ -2,10 +2,6 @@ import logging from collections.abc import Mapping from typing import Any -from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE -from core.workflow.file_reference import resolve_file_record_id -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID -from factories.variable_factory import build_segment_with_type from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus from graphon.file import FileTransferMethod from graphon.node_events import NodeRunResult @@ -14,6 +10,11 @@ from graphon.nodes.protocols import FileReferenceFactoryProtocol from graphon.variables.types import SegmentType from graphon.variables.variables import FileVariable +from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE +from core.workflow.file_reference import resolve_file_record_id +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID +from factories.variable_factory import build_segment_with_type + from .entities import ContentType, WebhookData logger = logging.getLogger(__name__) diff --git a/api/events/event_handlers/create_document_index.py b/api/events/event_handlers/create_document_index.py index 0c535a1c5b..b7e7a6e60f 100644 --- a/api/events/event_handlers/create_document_index.py +++ b/api/events/event_handlers/create_document_index.py @@ -6,9 +6,9 @@ import click from sqlalchemy import select from werkzeug.exceptions import NotFound -from core.db.session_factory import session_factory from core.indexing_runner import DocumentIsPausedError, IndexingRunner from events.document_index_event import document_index_created +from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from models.dataset import Document from models.enums import IndexingStatus @@ -22,25 +22,24 @@ def handle(sender, **kwargs): document_ids = kwargs.get("document_ids", []) documents = [] start_at = time.perf_counter() - with session_factory.create_session() as session: - for document_id in document_ids: - logger.info(click.style(f"Start process document: {document_id}", fg="green")) + for document_id in document_ids: + logger.info(click.style(f"Start process document: {document_id}", fg="green")) - document = session.scalar( - select(Document).where( - Document.id == document_id, - Document.dataset_id == dataset_id, - ) + document = db.session.scalar( + select(Document).where( + Document.id == document_id, + Document.dataset_id == dataset_id, ) + ) - if not document: - raise NotFound("Document not found") + if not document: + raise NotFound("Document not found") - document.indexing_status = IndexingStatus.PARSING - document.processing_started_at = naive_utc_now() - documents.append(document) - session.add(document) - session.commit() + document.indexing_status = IndexingStatus.PARSING + document.processing_started_at = naive_utc_now() + documents.append(document) + db.session.add(document) + db.session.commit() with contextlib.suppress(Exception): try: diff --git a/api/events/event_handlers/create_site_record_when_app_created.py b/api/events/event_handlers/create_site_record_when_app_created.py index 5e2a456dce..84be592b1a 100644 --- a/api/events/event_handlers/create_site_record_when_app_created.py +++ b/api/events/event_handlers/create_site_record_when_app_created.py @@ -1,5 +1,5 @@ -from core.db.session_factory import session_factory from events.app_event import app_was_created +from extensions.ext_database import db from models.enums import CustomizeTokenStrategy from models.model import Site @@ -22,6 +22,6 @@ def handle(sender, **kwargs): created_by=app.created_by, updated_by=app.updated_by, ) - with session_factory.create_session() as session: - session.add(site) - session.commit() + + db.session.add(site) + db.session.commit() diff --git a/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py b/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py index ba9758175f..7bd8e88231 100644 --- a/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py +++ b/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py @@ -1,11 +1,12 @@ import logging +from graphon.nodes import BuiltinNodeTypes +from graphon.nodes.tool.entities import ToolEntity + from core.tools.entities.tool_entities import ToolProviderType from core.tools.tool_manager import ToolManager from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_draft_workflow_was_synced -from graphon.nodes import BuiltinNodeTypes -from graphon.nodes.tool.entities import ToolEntity logger = logging.getLogger(__name__) diff --git a/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py b/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py index 6769b94cde..86b5b2bbf0 100644 --- a/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py +++ b/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py @@ -1,11 +1,11 @@ from typing import cast +from graphon.nodes import BuiltinNodeTypes from sqlalchemy import delete, select from core.workflow.nodes.knowledge_retrieval.entities import KnowledgeRetrievalNodeData from events.app_event import app_published_workflow_was_updated from extensions.ext_database import db -from graphon.nodes import BuiltinNodeTypes from models.dataset import AppDatasetJoin from models.workflow import Workflow diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py index 69d1f1ab07..5cc58f27c4 100644 --- a/api/extensions/ext_sentry.py +++ b/api/extensions/ext_sentry.py @@ -5,12 +5,11 @@ from dify_app import DifyApp def init_app(app: DifyApp): if dify_config.SENTRY_DSN: import sentry_sdk + from graphon.model_runtime.errors.invoke import InvokeRateLimitError from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.flask import FlaskIntegration from werkzeug.exceptions import HTTPException - from graphon.model_runtime.errors.invoke import InvokeRateLimitError - try: from langfuse._utils import parse_error diff --git a/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py b/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py index 64ff0f0674..db599c5d49 100644 --- a/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py +++ b/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py @@ -11,12 +11,12 @@ from collections.abc import Sequence from datetime import datetime from typing import Any +from graphon.enums import WorkflowNodeExecutionStatus from sqlalchemy.orm import sessionmaker from extensions.logstore.aliyun_logstore import AliyunLogStore from extensions.logstore.repositories import safe_float, safe_int from extensions.logstore.sql_escape import escape_identifier, escape_logstore_query_value -from graphon.enums import WorkflowNodeExecutionStatus from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom from repositories.api_workflow_node_execution_repository import DifyAPIWorkflowNodeExecutionRepository diff --git a/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py b/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py index 7f77a0437a..2745141431 100644 --- a/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py +++ b/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py @@ -20,12 +20,12 @@ from collections.abc import Sequence from datetime import datetime from typing import Any, cast +from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import sessionmaker from extensions.logstore.aliyun_logstore import AliyunLogStore from extensions.logstore.repositories import safe_float, safe_int from extensions.logstore.sql_escape import escape_identifier, escape_logstore_query_value, escape_sql_string -from graphon.enums import WorkflowExecutionStatus from libs.infinite_scroll_pagination import InfiniteScrollPagination from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowRun, WorkflowType diff --git a/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py b/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py index 544109276d..d0f3e2e244 100644 --- a/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py +++ b/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py @@ -3,14 +3,14 @@ import logging import os import time +from graphon.entities import WorkflowExecution +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.factory import WorkflowExecutionRepository from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository from extensions.logstore.aliyun_logstore import AliyunLogStore -from graphon.entities import WorkflowExecution -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from models import ( Account, diff --git a/api/extensions/otel/parser/base.py b/api/extensions/otel/parser/base.py index fbf379b3e5..23d324f9ea 100644 --- a/api/extensions/otel/parser/base.py +++ b/api/extensions/otel/parser/base.py @@ -10,17 +10,17 @@ Gate is only active in EE (``ENTERPRISE_ENABLED=True``) when import json from typing import Any, Protocol +from graphon.enums import BuiltinNodeTypes +from graphon.file import File +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node +from graphon.variables import Segment from opentelemetry.trace import Span from opentelemetry.trace.status import Status, StatusCode from pydantic import BaseModel from configs import dify_config from extensions.otel.semconv.gen_ai import ChainAttributes, GenAIAttributes -from graphon.enums import BuiltinNodeTypes -from graphon.file import File -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node -from graphon.variables import Segment def should_include_content() -> bool: diff --git a/api/extensions/otel/parser/llm.py b/api/extensions/otel/parser/llm.py index ec3c78a12d..335c5cc29e 100644 --- a/api/extensions/otel/parser/llm.py +++ b/api/extensions/otel/parser/llm.py @@ -6,12 +6,12 @@ import logging from collections.abc import Mapping from typing import Any +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node from opentelemetry.trace import Span from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps from extensions.otel.semconv.gen_ai import LLMAttributes -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node logger = logging.getLogger(__name__) diff --git a/api/extensions/otel/parser/retrieval.py b/api/extensions/otel/parser/retrieval.py index 56672d1fd4..6df5f62c15 100644 --- a/api/extensions/otel/parser/retrieval.py +++ b/api/extensions/otel/parser/retrieval.py @@ -6,13 +6,13 @@ import logging from collections.abc import Sequence from typing import Any +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node +from graphon.variables import Segment from opentelemetry.trace import Span from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps from extensions.otel.semconv.gen_ai import RetrieverAttributes -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node -from graphon.variables import Segment logger = logging.getLogger(__name__) diff --git a/api/extensions/otel/parser/tool.py b/api/extensions/otel/parser/tool.py index 75ddbba448..b9fdd9e1ca 100644 --- a/api/extensions/otel/parser/tool.py +++ b/api/extensions/otel/parser/tool.py @@ -2,14 +2,14 @@ Parser for tool nodes that captures tool-specific metadata. """ -from opentelemetry.trace import Span - -from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps -from extensions.otel.semconv.gen_ai import ToolAttributes from graphon.enums import WorkflowNodeExecutionMetadataKey from graphon.graph_events import GraphNodeEventBase from graphon.nodes.base.node import Node from graphon.nodes.tool.entities import ToolNodeData +from opentelemetry.trace import Span + +from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps +from extensions.otel.semconv.gen_ai import ToolAttributes class ToolNodeOTelParser: diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index fd7acb14d3..57205b5739 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -8,11 +8,6 @@ shared conversion functions for legacy callers and tests. from collections.abc import Mapping, Sequence from typing import Any, cast -from configs import dify_config -from core.workflow.variable_prefixes import ( - CONVERSATION_VARIABLE_NODE_ID, - ENVIRONMENT_VARIABLE_NODE_ID, -) from graphon.variables.exc import VariableError from graphon.variables.factory import ( TypeMismatchError, @@ -36,6 +31,12 @@ from graphon.variables.variables import ( VariableBase, ) +from configs import dify_config +from core.workflow.variable_prefixes import ( + CONVERSATION_VARIABLE_NODE_ID, + ENVIRONMENT_VARIABLE_NODE_ID, +) + __all__ = [ "TypeMismatchError", "UnsupportedSegmentTypeError", diff --git a/api/fields/member_fields.py b/api/fields/member_fields.py index 67b320beaa..cfe0015918 100644 --- a/api/fields/member_fields.py +++ b/api/fields/member_fields.py @@ -3,10 +3,10 @@ from __future__ import annotations from datetime import datetime from flask_restx import fields +from graphon.file import helpers as file_helpers from pydantic import computed_field, field_validator from fields.base import ResponseModel -from graphon.file import helpers as file_helpers simple_account_fields = { "id": fields.String, diff --git a/api/fields/message_fields.py b/api/fields/message_fields.py index ca18f1c203..1a871204a0 100644 --- a/api/fields/message_fields.py +++ b/api/fields/message_fields.py @@ -3,12 +3,12 @@ from __future__ import annotations from datetime import datetime from uuid import uuid4 +from graphon.file import File from pydantic import Field, field_validator from core.entities.execution_extra_content import ExecutionExtraContentDomainModel from fields.base import ResponseModel from fields.conversation_fields import AgentThought, JSONValue, MessageFile -from graphon.file import File type JSONValueType = JSONValue diff --git a/api/fields/raws.py b/api/fields/raws.py index ee6f53b360..4c65cdab7a 100644 --- a/api/fields/raws.py +++ b/api/fields/raws.py @@ -1,5 +1,4 @@ from flask_restx import fields - from graphon.file import File diff --git a/api/fields/workflow_fields.py b/api/fields/workflow_fields.py index 6e947858ba..3af42eabd8 100644 --- a/api/fields/workflow_fields.py +++ b/api/fields/workflow_fields.py @@ -1,8 +1,8 @@ from flask_restx import fields +from graphon.variables import SecretVariable, SegmentType, VariableBase from core.helper import encrypter from fields.member_fields import simple_account_fields -from graphon.variables import SecretVariable, SegmentType, VariableBase from libs.helper import TimestampField from ._value_type_serializer import serialize_value_type @@ -68,6 +68,7 @@ pipeline_variable_fields = { workflow_fields = { "id": fields.String, + "kind": fields.String(attribute="kind_or_standard"), "graph": fields.Raw(attribute="graph_dict"), "features": fields.Raw(attribute="features_dict"), "hash": fields.String(attribute="unique_hash"), diff --git a/api/libs/helper.py b/api/libs/helper.py index ac69a11084..69bd483515 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -16,6 +16,8 @@ from zoneinfo import available_timezones from flask import Response, stream_with_context from flask_restx import fields +from graphon.file import helpers as file_helpers +from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, TypeAdapter from pydantic.functional_validators import AfterValidator from typing_extensions import TypedDict @@ -23,8 +25,6 @@ from typing_extensions import TypedDict from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from extensions.ext_redis import redis_client -from graphon.file import helpers as file_helpers -from graphon.model_runtime.utils.encoders import jsonable_encoder if TYPE_CHECKING: from models import Account diff --git a/api/models/__init__.py b/api/models/__init__.py index f6e43bb4ea..2055a6b005 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -118,6 +118,7 @@ from .workflow import ( WorkflowAppLog, WorkflowAppLogCreatedFrom, WorkflowArchiveLog, + WorkflowKind, WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload, WorkflowNodeExecutionTriggeredFrom, @@ -240,5 +241,6 @@ __all__ = [ "WorkflowSchedulePlan", "WorkflowToolProvider", "WorkflowTriggerStatus", + "WorkflowKind", "WorkflowType", ] diff --git a/api/models/dataset.py b/api/models/dataset.py index eee5c39a0e..50301dd2d7 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -1715,7 +1715,7 @@ class SegmentAttachmentBinding(TypeBase): ) -class DocumentSegmentSummary(TypeBase): +class DocumentSegmentSummary(Base): __tablename__ = "document_segment_summaries" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="document_segment_summaries_pkey"), @@ -1725,40 +1725,25 @@ class DocumentSegmentSummary(TypeBase): sa.Index("document_segment_summaries_status_idx", "status"), ) - id: Mapped[str] = mapped_column( - StringUUID, - nullable=False, - insert_default=lambda: str(uuid4()), - default_factory=lambda: str(uuid4()), - init=False, - ) + id: Mapped[str] = mapped_column(StringUUID, nullable=False, default=lambda: str(uuid4())) dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False) document_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # corresponds to DocumentSegment.id or parent chunk id chunk_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - summary_content: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None) - summary_index_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) - summary_index_node_hash: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) - tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True, default=None) - status: Mapped[SummaryStatus] = mapped_column( - EnumText(SummaryStatus, length=32), - nullable=False, - server_default=sa.text("'generating'"), - default=SummaryStatus.GENERATING, - ) - error: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None) - enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"), default=True) - disabled_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None) - disabled_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None) - created_at: Mapped[datetime] = mapped_column( - DateTime, nullable=False, server_default=func.current_timestamp(), init=False + summary_content: Mapped[str] = mapped_column(LongText, nullable=True) + summary_index_node_id: Mapped[str] = mapped_column(String(255), nullable=True) + summary_index_node_hash: Mapped[str] = mapped_column(String(255), nullable=True) + tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True) + status: Mapped[str] = mapped_column( + EnumText(SummaryStatus, length=32), nullable=False, server_default=sa.text("'generating'") ) + error: Mapped[str] = mapped_column(LongText, nullable=True) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) + disabled_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + disabled_by = mapped_column(StringUUID, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column( - DateTime, - nullable=False, - server_default=func.current_timestamp(), - onupdate=func.current_timestamp(), - init=False, + DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() ) def __repr__(self): diff --git a/api/models/evaluation.py b/api/models/evaluation.py index fce50c5f48..680d6ab31c 100644 --- a/api/models/evaluation.py +++ b/api/models/evaluation.py @@ -85,7 +85,7 @@ class EvaluationConfiguration(Base): """Return judgment config (stored in the judgement_conditions column).""" if self.judgement_conditions: parsed = json.loads(self.judgement_conditions) - return parsed or None + return parsed if parsed else None return None @property diff --git a/api/models/model.py b/api/models/model.py index 7fe0731098..8eabf45363 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -14,6 +14,9 @@ from uuid import uuid4 import sqlalchemy as sa from flask import request from flask_login import UserMixin # type: ignore[import-untyped] +from graphon.enums import WorkflowExecutionStatus +from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType +from graphon.file import helpers as file_helpers from sqlalchemy import BigInteger, Float, Index, PrimaryKeyConstraint, String, exists, func, select, text from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker @@ -21,9 +24,6 @@ from configs import dify_config from constants import DEFAULT_FILE_NUMBER_LIMITS from core.tools.signature import sign_tool_file from extensions.storage.storage_type import StorageType -from graphon.enums import WorkflowExecutionStatus -from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType -from graphon.file import helpers as file_helpers from libs.helper import generate_string # type: ignore[import-not-found] from libs.uuid_utils import uuidv7 from models.utils.file_input_compat import build_file_from_input_mapping diff --git a/api/models/provider.py b/api/models/provider.py index 2bb67d605b..8270961b31 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -6,10 +6,10 @@ from functools import cached_property from uuid import uuid4 import sqlalchemy as sa +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import DateTime, String, func, select, text from sqlalchemy.orm import Mapped, mapped_column -from graphon.model_runtime.entities.model_entities import ModelType from libs.uuid_utils import uuidv7 from .base import TypeBase diff --git a/api/models/workflow.py b/api/models/workflow.py index d25fa488e8..bb4c24380f 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -8,6 +8,19 @@ from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast from uuid import uuid4 import sqlalchemy as sa +from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter +from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause +from graphon.enums import ( + BuiltinNodeTypes, + NodeType, + WorkflowExecutionStatus, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) +from graphon.file import File +from graphon.file.constants import maybe_file_object +from graphon.variables import utils as variable_utils +from graphon.variables.variables import FloatVariable, IntegerVariable, RAGPipelineVariable, StringVariable from sqlalchemy import ( DateTime, Index, @@ -31,19 +44,6 @@ from core.workflow.variable_prefixes import ( ) from extensions.ext_storage import Storage from factories.variable_factory import TypeMismatchError, build_segment_with_type -from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter -from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause -from graphon.enums import ( - BuiltinNodeTypes, - NodeType, - WorkflowExecutionStatus, - WorkflowNodeExecutionMetadataKey, - WorkflowNodeExecutionStatus, -) -from graphon.file import File -from graphon.file.constants import maybe_file_object -from graphon.variables import utils as variable_utils -from graphon.variables.variables import FloatVariable, IntegerVariable, RAGPipelineVariable, StringVariable from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 @@ -53,10 +53,11 @@ if TYPE_CHECKING: from .model import AppMode, UploadFile +from graphon.variables import SecretVariable, Segment, SegmentType, VariableBase + from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from core.helper import encrypter from factories import variable_factory -from graphon.variables import SecretVariable, Segment, SegmentType, VariableBase from libs import helper from .account import Account @@ -138,6 +139,34 @@ class WorkflowType(StrEnum): return cls.WORKFLOW if app_mode == AppMode.WORKFLOW else cls.CHAT +class WorkflowKind(StrEnum): + """ + Workflow business kind. + + Runtime execution type and product business kind are intentionally separated: + - ``Workflow.type`` is consumed by the graph runtime layer. + - ``Workflow.kind`` is consumed by product logic (snippet/evaluation/standard). + """ + + STANDARD = "standard" + SNIPPET = "snippet" + EVALUATION = "evaluation" + + @classmethod + def value_of(cls, value: str) -> "WorkflowKind": + for kind in cls: + if kind.value == value: + return kind + raise ValueError(f"invalid workflow kind value {value}") + + +def resolve_workflow_kind(kind: "WorkflowKind | str | None") -> WorkflowKind: + """Resolve workflow business kind, defaulting empty values to ``standard``.""" + if kind: + return kind if isinstance(kind, WorkflowKind) else WorkflowKind.value_of(kind) + return WorkflowKind.STANDARD + + class _InvalidGraphDefinitionError(Exception): pass @@ -151,12 +180,14 @@ class Workflow(Base): # bug - id (uuid) Workflow ID, pk - tenant_id (uuid) Workspace ID - app_id (uuid) App ID - - type (string) Workflow type + - type (string) Runtime workflow type `workflow` for `Workflow App` `chat` for `Chat App workflow mode` + - kind (string) Business workflow kind (`standard`/`snippet`/`evaluation`) + - version (string) Version `draft` for draft version (only one for each app), other for version number (redundant) @@ -185,6 +216,12 @@ class Workflow(Base): # bug tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) type: Mapped[WorkflowType] = mapped_column(EnumText(WorkflowType, length=255), nullable=False) + kind: Mapped[WorkflowKind | None] = mapped_column( + EnumText(WorkflowKind, length=255), + nullable=True, + default=WorkflowKind.STANDARD, + server_default=sa.text("'standard'"), + ) version: Mapped[str] = mapped_column(String(255), nullable=False) marked_name: Mapped[str] = mapped_column(String(255), default="", server_default="") marked_comment: Mapped[str] = mapped_column(String(255), default="", server_default="") @@ -224,6 +261,7 @@ class Workflow(Base): # bug environment_variables: Sequence[VariableBase], conversation_variables: Sequence[VariableBase], rag_pipeline_variables: list[dict], + kind: str | None = WorkflowKind.STANDARD.value, marked_name: str = "", marked_comment: str = "", ) -> "Workflow": @@ -232,6 +270,7 @@ class Workflow(Base): # bug workflow.tenant_id = tenant_id workflow.app_id = app_id workflow.type = WorkflowType(type) + workflow.kind = WorkflowKind(kind) if kind else WorkflowKind.STANDARD workflow.version = version workflow.graph = graph workflow.features = features @@ -253,6 +292,14 @@ class Workflow(Base): # bug def updated_by_account(self): return db.session.get(Account, self.updated_by) if self.updated_by else None + @property + def kind_or_standard(self) -> str: + return self.resolved_kind.value + + @property + def resolved_kind(self) -> WorkflowKind: + return resolve_workflow_kind(self.kind) + @property def graph_dict(self) -> Mapping[str, Any]: # TODO(QuantumGhost): Consider caching `graph_dict` to avoid repeated JSON decoding. diff --git a/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/aliyun_trace.py b/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/aliyun_trace.py index 54d2f8167f..eb44d437f9 100644 --- a/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/aliyun_trace.py +++ b/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/aliyun_trace.py @@ -1,6 +1,8 @@ import logging from collections.abc import Sequence +from graphon.entities import WorkflowNodeExecution +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from opentelemetry.trace import SpanKind from sqlalchemy.orm import sessionmaker @@ -58,8 +60,6 @@ from dify_trace_aliyun.utils import ( serialize_json_data, ) from extensions.ext_database import db -from graphon.entities import WorkflowNodeExecution -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/utils.py b/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/utils.py index 5678c66adb..d446f1b5c3 100644 --- a/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/utils.py +++ b/api/providers/trace/trace-aliyun/src/dify_trace_aliyun/utils.py @@ -2,6 +2,8 @@ import json from collections.abc import Mapping from typing import Any, TypedDict +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionStatus from opentelemetry.trace import Link, Status, StatusCode from core.rag.models.document import Document @@ -15,8 +17,6 @@ from dify_trace_aliyun.entities.semconv import ( GenAISpanKind, ) from extensions.ext_database import db -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionStatus from models import EndUser # Constants diff --git a/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py b/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py index 96df49ed0e..037023e771 100644 --- a/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py +++ b/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from typing import Any, Union, cast from urllib.parse import urlparse +from graphon.enums import WorkflowNodeExecutionStatus from openinference.semconv.trace import ( MessageAttributes, OpenInferenceMimeTypeValues, @@ -40,7 +41,6 @@ from core.ops.utils import JSON_DICT_ADAPTER from core.repositories import DifyCoreRepositoryFactory from dify_trace_arize_phoenix.config import ArizeConfig, PhoenixConfig from extensions.ext_database import db -from graphon.enums import WorkflowNodeExecutionStatus from models.model import EndUser, MessageFile from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py b/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py index 145bd70dbc..504673d6ef 100644 --- a/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py +++ b/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py @@ -4,6 +4,7 @@ import uuid from datetime import datetime, timedelta from typing import cast +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from langsmith import Client from langsmith.schemas import RunBase from sqlalchemy.orm import sessionmaker @@ -29,7 +30,6 @@ from dify_trace_langsmith.entities.langsmith_trace_entity import ( LangSmithRunUpdateModel, ) from extensions.ext_database import db -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/providers/trace/trace-mlflow/src/dify_trace_mlflow/mlflow_trace.py b/api/providers/trace/trace-mlflow/src/dify_trace_mlflow/mlflow_trace.py index 4e4c45a532..37c9ecda25 100644 --- a/api/providers/trace/trace-mlflow/src/dify_trace_mlflow/mlflow_trace.py +++ b/api/providers/trace/trace-mlflow/src/dify_trace_mlflow/mlflow_trace.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from typing import Any, cast import mlflow +from graphon.enums import BuiltinNodeTypes from mlflow.entities import Document, Span, SpanEvent, SpanStatusCode, SpanType from mlflow.tracing.constant import SpanAttributeKey, TokenUsageKey, TraceMetadataKey from mlflow.tracing.fluent import start_span_no_context, update_current_trace @@ -25,7 +26,6 @@ from core.ops.entities.trace_entity import ( from core.ops.utils import JSON_DICT_ADAPTER from dify_trace_mlflow.config import DatabricksConfig, MLflowConfig from extensions.ext_database import db -from graphon.enums import BuiltinNodeTypes from models import EndUser from models.workflow import WorkflowNodeExecutionModel diff --git a/api/providers/trace/trace-opik/src/dify_trace_opik/opik_trace.py b/api/providers/trace/trace-opik/src/dify_trace_opik/opik_trace.py index 2d124ac989..fd2d0ca097 100644 --- a/api/providers/trace/trace-opik/src/dify_trace_opik/opik_trace.py +++ b/api/providers/trace/trace-opik/src/dify_trace_opik/opik_trace.py @@ -5,6 +5,7 @@ import uuid from datetime import datetime, timedelta from typing import Any, cast +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from opik import Opik, Trace from opik.id_helpers import uuid4_to_uuid7 from sqlalchemy.orm import sessionmaker @@ -24,7 +25,6 @@ from core.ops.entities.trace_entity import ( from core.repositories import DifyCoreRepositoryFactory from dify_trace_opik.config import OpikConfig from extensions.ext_database import db -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/providers/trace/trace-tencent/src/dify_trace_tencent/tencent_trace.py b/api/providers/trace/trace-tencent/src/dify_trace_tencent/tencent_trace.py index a8c480e4a5..3b9e24573a 100644 --- a/api/providers/trace/trace-tencent/src/dify_trace_tencent/tencent_trace.py +++ b/api/providers/trace/trace-tencent/src/dify_trace_tencent/tencent_trace.py @@ -3,6 +3,10 @@ import inspect import logging +from graphon.entities.workflow_node_execution import ( + WorkflowNodeExecution, +) +from graphon.nodes import BuiltinNodeTypes from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -24,10 +28,6 @@ from dify_trace_tencent.entities.tencent_trace_entity import SpanData from dify_trace_tencent.span_builder import TencentSpanBuilder from dify_trace_tencent.utils import TencentTraceUtils from extensions.ext_database import db -from graphon.entities.workflow_node_execution import ( - WorkflowNodeExecution, -) -from graphon.nodes import BuiltinNodeTypes from models import Account, App, TenantAccountJoin, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/providers/trace/trace-weave/src/dify_trace_weave/weave_trace.py b/api/providers/trace/trace-weave/src/dify_trace_weave/weave_trace.py index 4292cbf0f1..fd9b965ba0 100644 --- a/api/providers/trace/trace-weave/src/dify_trace_weave/weave_trace.py +++ b/api/providers/trace/trace-weave/src/dify_trace_weave/weave_trace.py @@ -6,6 +6,7 @@ from typing import Any, cast import wandb import weave +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from sqlalchemy.orm import sessionmaker from weave.trace_server.trace_server_interface import ( CallEndReq, @@ -32,7 +33,6 @@ from core.repositories import DifyCoreRepositoryFactory from dify_trace_weave.config import WeaveConfig from dify_trace_weave.entities.weave_trace_entity import WeaveTraceModel from extensions.ext_database import db -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/repositories/api_workflow_run_repository.py b/api/repositories/api_workflow_run_repository.py index 72b38e7906..100589804c 100644 --- a/api/repositories/api_workflow_run_repository.py +++ b/api/repositories/api_workflow_run_repository.py @@ -38,11 +38,11 @@ from collections.abc import Callable, Sequence from datetime import datetime from typing import Protocol, TypedDict +from graphon.entities.pause_reason import PauseReason +from graphon.enums import WorkflowType from sqlalchemy.orm import Session from core.repositories.factory import WorkflowExecutionRepository -from graphon.entities.pause_reason import PauseReason -from graphon.enums import WorkflowType from libs.infinite_scroll_pagination import InfiniteScrollPagination from models.enums import WorkflowRunTriggeredFrom from models.workflow import WorkflowAppLog, WorkflowArchiveLog, WorkflowPause, WorkflowPauseReason, WorkflowRun diff --git a/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py b/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py index 44735eb769..d5c6a203b1 100644 --- a/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py +++ b/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py @@ -10,11 +10,11 @@ from collections.abc import Sequence from datetime import datetime from typing import Protocol, cast +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from sqlalchemy import asc, delete, desc, func, select from sqlalchemy.engine import CursorResult from sqlalchemy.orm import Session, sessionmaker -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload from repositories.api_workflow_node_execution_repository import ( DifyAPIWorkflowNodeExecutionRepository, diff --git a/api/repositories/sqlalchemy_api_workflow_run_repository.py b/api/repositories/sqlalchemy_api_workflow_run_repository.py index 474b200fc5..b760696c5e 100644 --- a/api/repositories/sqlalchemy_api_workflow_run_repository.py +++ b/api/repositories/sqlalchemy_api_workflow_run_repository.py @@ -28,15 +28,15 @@ from decimal import Decimal from typing import Any, cast import sqlalchemy as sa +from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause +from graphon.enums import WorkflowExecutionStatus, WorkflowType +from graphon.nodes.human_input.entities import FormDefinition from pydantic import ValidationError from sqlalchemy import and_, delete, func, null, or_, select, tuple_ from sqlalchemy.engine import CursorResult from sqlalchemy.orm import Session, selectinload, sessionmaker from extensions.ext_storage import storage -from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause -from graphon.enums import WorkflowExecutionStatus, WorkflowType -from graphon.nodes.human_input.entities import FormDefinition from libs.datetime_utils import naive_utc_now from libs.helper import convert_datetime_to_date from libs.infinite_scroll_pagination import InfiniteScrollPagination diff --git a/api/repositories/sqlalchemy_execution_extra_content_repository.py b/api/repositories/sqlalchemy_execution_extra_content_repository.py index 67f8795d3f..feba5f7eb6 100644 --- a/api/repositories/sqlalchemy_execution_extra_content_repository.py +++ b/api/repositories/sqlalchemy_execution_extra_content_repository.py @@ -7,6 +7,9 @@ from collections import defaultdict from collections.abc import Sequence from typing import Any +from graphon.nodes.human_input.entities import FormDefinition +from graphon.nodes.human_input.enums import HumanInputFormStatus +from graphon.nodes.human_input.human_input_node import HumanInputNode from sqlalchemy import select from sqlalchemy.orm import Session, selectinload, sessionmaker @@ -18,9 +21,6 @@ from core.entities.execution_extra_content import ( from core.entities.execution_extra_content import ( HumanInputContent as HumanInputContentDomainModel, ) -from graphon.nodes.human_input.entities import FormDefinition -from graphon.nodes.human_input.enums import HumanInputFormStatus -from graphon.nodes.human_input.human_input_node import HumanInputNode from models.execution_extra_content import ( ExecutionExtraContent as ExecutionExtraContentModel, ) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 97aaea3395..70184b42de 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -10,6 +10,12 @@ from uuid import uuid4 import yaml from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad +from graphon.enums import BuiltinNodeTypes +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes.llm.entities import LLMNodeData +from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData +from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData +from graphon.nodes.tool.entities import ToolNodeData from packaging import version from packaging.version import parse as parse_version from pydantic import BaseModel @@ -30,12 +36,6 @@ from core.workflow.nodes.trigger_schedule.trigger_schedule_node import TriggerSc from events.app_event import app_model_config_was_updated, app_was_created from extensions.ext_redis import redis_client from factories import variable_factory -from graphon.enums import BuiltinNodeTypes -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes.llm.entities import LLMNodeData -from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData -from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData -from graphon.nodes.tool.entities import ToolNodeData from libs.datetime_utils import naive_utc_now from models import Account, App, AppMode from models.model import AppModelConfig, AppModelConfigDict, IconType diff --git a/api/services/app_task_service.py b/api/services/app_task_service.py index 6e9d6b1c73..0842e9d3e7 100644 --- a/api/services/app_task_service.py +++ b/api/services/app_task_service.py @@ -5,10 +5,11 @@ like stopping tasks, handling both legacy Redis flag mechanism and new GraphEngine command channel mechanism. """ +from graphon.graph_engine.manager import GraphEngineManager + from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_redis import redis_client -from graphon.graph_engine.manager import GraphEngineManager from models.model import AppMode diff --git a/api/services/audio_service.py b/api/services/audio_service.py index 60948e652b..1c7027efb4 100644 --- a/api/services/audio_service.py +++ b/api/services/audio_service.py @@ -5,12 +5,12 @@ from collections.abc import Generator from typing import cast from flask import Response, stream_with_context +from graphon.model_runtime.entities.model_entities import ModelType from werkzeug.datastructures import FileStorage from constants import AUDIO_EXTENSIONS from core.model_manager import ModelManager from extensions.ext_database import db -from graphon.model_runtime.entities.model_entities import ModelType from models.enums import MessageStatus from models.model import App, AppMode, Message from services.errors.audio import ( diff --git a/api/services/clear_free_plan_tenant_expired_logs.py b/api/services/clear_free_plan_tenant_expired_logs.py index dcc93b4b0f..ea12e40420 100644 --- a/api/services/clear_free_plan_tenant_expired_logs.py +++ b/api/services/clear_free_plan_tenant_expired_logs.py @@ -6,6 +6,7 @@ from concurrent.futures import ThreadPoolExecutor import click from flask import Flask, current_app +from graphon.model_runtime.utils.encoders import jsonable_encoder from sqlalchemy import delete, func, select from sqlalchemy.orm import Session, sessionmaker @@ -13,7 +14,6 @@ from configs import dify_config from enums.cloud_plan import CloudPlan from extensions.ext_database import db from extensions.ext_storage import storage -from graphon.model_runtime.utils.encoders import jsonable_encoder from models.account import Tenant from models.model import ( App, diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index ee8a1c4edd..f5085af59b 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -3,6 +3,7 @@ import logging from collections.abc import Callable, Sequence from typing import Any +from graphon.variables.types import SegmentType from sqlalchemy import asc, desc, func, or_, select from sqlalchemy.orm import Session @@ -12,7 +13,6 @@ from core.db.session_factory import session_factory from core.llm_generator.llm_generator import LLMGenerator from extensions.ext_database import db from factories import variable_factory -from graphon.variables.types import SegmentType from libs.datetime_utils import naive_utc_now from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account, ConversationVariable diff --git a/api/services/conversation_variable_updater.py b/api/services/conversation_variable_updater.py index 287d513f48..95a8951951 100644 --- a/api/services/conversation_variable_updater.py +++ b/api/services/conversation_variable_updater.py @@ -1,7 +1,7 @@ +from graphon.variables.variables import VariableBase from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker -from graphon.variables.variables import VariableBase from models import ConversationVariable diff --git a/api/services/datasource_provider_service.py b/api/services/datasource_provider_service.py index 416bc8cef9..364c4a86a0 100644 --- a/api/services/datasource_provider_service.py +++ b/api/services/datasource_provider_service.py @@ -3,6 +3,7 @@ import time from collections.abc import Mapping from typing import Any +from graphon.model_runtime.entities.provider_entities import FormType from sqlalchemy import delete, func, select, update from sqlalchemy.orm import Session, sessionmaker @@ -17,7 +18,6 @@ from core.plugin.impl.oauth import OAuthHandler from core.tools.utils.encryption import ProviderConfigCache, ProviderConfigEncrypter, create_provider_encrypter from extensions.ext_database import db from extensions.ext_redis import redis_client -from graphon.model_runtime.entities.provider_entities import FormType from models.oauth import DatasourceOauthParamConfig, DatasourceOauthTenantParamConfig, DatasourceProvider from models.provider_ids import DatasourceProviderID from services.plugin.plugin_service import PluginService diff --git a/api/services/entities/model_provider_entities.py b/api/services/entities/model_provider_entities.py index 6679c08ebd..a944ef6acd 100644 --- a/api/services/entities/model_provider_entities.py +++ b/api/services/entities/model_provider_entities.py @@ -1,6 +1,15 @@ from collections.abc import Sequence from enum import StrEnum +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + ModelCredentialSchema, + ProviderCredentialSchema, + ProviderHelpEntity, + SimpleProviderEntity, +) from pydantic import BaseModel, ConfigDict, model_validator from configs import dify_config @@ -15,15 +24,6 @@ from core.entities.provider_entities import ( QuotaConfiguration, UnaddedModelConfiguration, ) -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - ModelCredentialSchema, - ProviderCredentialSchema, - ProviderHelpEntity, - SimpleProviderEntity, -) from models.provider import ProviderType diff --git a/api/services/file_service.py b/api/services/file_service.py index 52da2a7951..79a935de4b 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -8,6 +8,7 @@ from tempfile import NamedTemporaryFile from typing import Literal from zipfile import ZIP_DEFLATED, ZipFile +from graphon.file import helpers as file_helpers from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import NotFound @@ -23,7 +24,6 @@ from core.rag.extractor.extract_processor import ExtractProcessor from extensions.ext_database import db from extensions.ext_storage import storage from extensions.storage.storage_type import StorageType -from graphon.file import helpers as file_helpers from libs.datetime_utils import naive_utc_now from libs.helper import extract_tenant_id from models import Account diff --git a/api/services/human_input_delivery_test_service.py b/api/services/human_input_delivery_test_service.py index 8b4983e5f7..17b28bbe48 100644 --- a/api/services/human_input_delivery_test_service.py +++ b/api/services/human_input_delivery_test_service.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, field from enum import StrEnum from typing import Protocol +from graphon.runtime import VariablePool from sqlalchemy import Engine, select from sqlalchemy.orm import sessionmaker @@ -17,7 +18,6 @@ from core.workflow.human_input_adapter import ( ) from extensions.ext_database import db from extensions.ext_mail import mail -from graphon.runtime import VariablePool from libs.email_template_renderer import render_email_template from models import Account, TenantAccountJoin from services.feature_service import FeatureService diff --git a/api/services/human_input_service.py b/api/services/human_input_service.py index 76598d31ac..02a6620fc7 100644 --- a/api/services/human_input_service.py +++ b/api/services/human_input_service.py @@ -3,6 +3,12 @@ from collections.abc import Mapping from datetime import datetime, timedelta from typing import Any +from graphon.nodes.human_input.entities import ( + FormDefinition, + HumanInputSubmissionValidationError, + validate_human_input_submission, +) +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker @@ -11,12 +17,6 @@ from core.repositories.human_input_repository import ( HumanInputFormRecord, HumanInputFormSubmissionRepository, ) -from graphon.nodes.human_input.entities import ( - FormDefinition, - HumanInputSubmissionValidationError, - validate_human_input_submission, -) -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import ensure_naive_utc, naive_utc_now from libs.exception import BaseHTTPException from models.human_input import RecipientType diff --git a/api/services/model_load_balancing_service.py b/api/services/model_load_balancing_service.py index c269346f5f..b652e049ce 100644 --- a/api/services/model_load_balancing_service.py +++ b/api/services/model_load_balancing_service.py @@ -2,6 +2,12 @@ import json import logging from typing import Any, TypedDict +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ModelCredentialSchema, + ProviderCredentialSchema, +) +from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from sqlalchemy import or_, select from constants import HIDDEN_VALUE @@ -12,12 +18,6 @@ from core.model_manager import LBModelManager from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly, create_plugin_provider_manager from core.provider_manager import ProviderManager from extensions.ext_database import db -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ModelCredentialSchema, - ProviderCredentialSchema, -) -from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from libs.datetime_utils import naive_utc_now from models.enums import CredentialSourceType from models.provider import LoadBalancingModelConfig, ProviderCredential, ProviderModelCredential diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index 9db6682e10..eac1093299 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -9,6 +9,15 @@ from typing import Any, cast from uuid import uuid4 from flask_login import current_user +from graphon.entities import WorkflowNodeExecution +from graphon.enums import BuiltinNodeTypes, ErrorStrategy, NodeType, WorkflowNodeExecutionStatus +from graphon.errors import WorkflowNodeRunFailedError +from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent +from graphon.node_events import NodeRunResult +from graphon.nodes.base.node import Node +from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config +from graphon.runtime import VariablePool +from graphon.variables.variables import Variable, VariableBase from sqlalchemy import func, select from sqlalchemy.orm import Session, sessionmaker @@ -44,15 +53,6 @@ from core.workflow.variable_pool_initializer import add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry from enterprise.telemetry.draft_trace import enqueue_draft_node_execution_trace from extensions.ext_database import db -from graphon.entities import WorkflowNodeExecution -from graphon.enums import BuiltinNodeTypes, ErrorStrategy, NodeType, WorkflowNodeExecutionStatus -from graphon.errors import WorkflowNodeRunFailedError -from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent -from graphon.node_events import NodeRunResult -from graphon.nodes.base.node import Node -from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config -from graphon.runtime import VariablePool -from graphon.variables.variables import Variable, VariableBase from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account from models.dataset import ( # type: ignore diff --git a/api/services/rag_pipeline/rag_pipeline_dsl_service.py b/api/services/rag_pipeline/rag_pipeline_dsl_service.py index f315d053cb..7dd86f1581 100644 --- a/api/services/rag_pipeline/rag_pipeline_dsl_service.py +++ b/api/services/rag_pipeline/rag_pipeline_dsl_service.py @@ -13,6 +13,12 @@ import yaml # type: ignore from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from flask_login import current_user +from graphon.enums import BuiltinNodeTypes +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes.llm.entities import LLMNodeData +from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData +from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData +from graphon.nodes.tool.entities import ToolNodeData from packaging import version from pydantic import BaseModel from sqlalchemy import select @@ -27,12 +33,6 @@ from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE from core.workflow.nodes.knowledge_retrieval.entities import KnowledgeRetrievalNodeData from extensions.ext_redis import redis_client from factories import variable_factory -from graphon.enums import BuiltinNodeTypes -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes.llm.entities import LLMNodeData -from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData -from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData -from graphon.nodes.tool.entities import ToolNodeData from models import Account from models.dataset import Dataset, DatasetCollectionBinding, Pipeline from models.enums import CollectionBindingType, DatasetRuntimeMode diff --git a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py index 21be411bea..ab60986bfe 100644 --- a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py +++ b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py @@ -27,13 +27,13 @@ from dataclasses import dataclass, field from typing import Any, TypedDict import click +from graphon.enums import WorkflowType from sqlalchemy import inspect from sqlalchemy.orm import Session, sessionmaker from configs import dify_config from enums.cloud_plan import CloudPlan from extensions.ext_database import db -from graphon.enums import WorkflowType from libs.archive_storage import ( ArchiveStorage, ArchiveStorageNotConfiguredError, diff --git a/api/services/snippet_service.py b/api/services/snippet_service.py index a2cdc23f3d..0d525a6248 100644 --- a/api/services/snippet_service.py +++ b/api/services/snippet_service.py @@ -16,6 +16,7 @@ from models.enums import WorkflowRunTriggeredFrom from models.snippet import CustomizedSnippet, SnippetType from models.workflow import ( Workflow, + WorkflowKind, WorkflowNodeExecutionModel, WorkflowRun, WorkflowType, @@ -47,6 +48,11 @@ class SnippetService: ) self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) + @staticmethod + def _snippet_kind_filter(): + """Match snippet workflows by business kind.""" + return Workflow.kind == WorkflowKind.SNIPPET.value + @staticmethod def validate_snippet_graph_forbidden_nodes(graph: Mapping[str, Any]) -> None: """Reject graphs that contain node types not allowed in snippets.""" @@ -271,7 +277,7 @@ class SnippetService: .where( Workflow.tenant_id == snippet.tenant_id, Workflow.app_id == snippet.id, - Workflow.type == WorkflowType.SNIPPET.value, + self._snippet_kind_filter(), Workflow.version == "draft", ) .first() @@ -293,7 +299,7 @@ class SnippetService: .where( Workflow.tenant_id == snippet.tenant_id, Workflow.app_id == snippet.id, - Workflow.type == WorkflowType.SNIPPET.value, + self._snippet_kind_filter(), Workflow.id == snippet.workflow_id, ) .first() @@ -336,7 +342,8 @@ class SnippetService: tenant_id=snippet.tenant_id, app_id=snippet.id, features="{}", - type=WorkflowType.SNIPPET.value, + type=WorkflowType.WORKFLOW.value, + kind=WorkflowKind.SNIPPET.value, version="draft", graph=json.dumps(graph), created_by=account.id, @@ -348,6 +355,8 @@ class SnippetService: else: # Update existing draft workflow workflow.graph = json.dumps(graph) + workflow.type = WorkflowType.WORKFLOW.value + workflow.kind = WorkflowKind.SNIPPET workflow.updated_by = account.id workflow.updated_at = datetime.now(UTC).replace(tzinfo=None) workflow.environment_variables = [] @@ -381,7 +390,7 @@ class SnippetService: draft_workflow_stmt = select(Workflow).where( Workflow.tenant_id == snippet.tenant_id, Workflow.app_id == snippet.id, - Workflow.type == WorkflowType.SNIPPET.value, + self._snippet_kind_filter(), Workflow.version == "draft", ) draft_workflow = session.scalar(draft_workflow_stmt) @@ -394,7 +403,7 @@ class SnippetService: workflow = Workflow.new( tenant_id=snippet.tenant_id, app_id=snippet.id, - type=draft_workflow.type, + type=WorkflowType.WORKFLOW.value, version=str(datetime.now(UTC).replace(tzinfo=None)), graph=draft_workflow.graph, features=draft_workflow.features, @@ -402,6 +411,7 @@ class SnippetService: environment_variables=[], conversation_variables=[], rag_pipeline_variables=draft_workflow.rag_pipeline_variables, + kind=WorkflowKind.SNIPPET.value, marked_name="", marked_comment="", ) @@ -440,7 +450,7 @@ class SnippetService: select(Workflow) .where( Workflow.app_id == snippet.id, - Workflow.type == WorkflowType.SNIPPET.value, + self._snippet_kind_filter(), Workflow.version != "draft", ) .order_by(Workflow.version.desc()) diff --git a/api/services/tools/api_tools_manage_service.py b/api/services/tools/api_tools_manage_service.py index 5ff2c21749..3bfa221528 100644 --- a/api/services/tools/api_tools_manage_service.py +++ b/api/services/tools/api_tools_manage_service.py @@ -2,9 +2,9 @@ import json import logging from typing import Any, TypedDict, cast +from graphon.model_runtime.utils.encoders import jsonable_encoder from httpx import get from sqlalchemy import select -from sqlalchemy.orm import sessionmaker from core.entities.provider_entities import ProviderConfig from core.tools.__base.tool_runtime import ToolRuntime @@ -16,13 +16,11 @@ from core.tools.entities.tool_entities import ( ApiProviderAuthType, ApiProviderSchemaType, ) -from core.tools.errors import ApiToolProviderNotFoundError from core.tools.tool_label_manager import ToolLabelManager from core.tools.tool_manager import ToolManager from core.tools.utils.encryption import create_tool_provider_encrypter from core.tools.utils.parser import ApiBasedToolSchemaParser from extensions.ext_database import db -from graphon.model_runtime.utils.encoders import jsonable_encoder from models.tools import ApiToolProvider from services.tools.tools_transform_service import ToolTransformService @@ -118,85 +116,71 @@ class ApiToolManageService: privacy_policy: str, custom_disclaimer: str, labels: list[str], - ) -> dict[str, Any]: + ): """ - Create a new API tool provider. - - :param user_id: The ID of the user creating the provider. - :param tenant_id: The ID of the workspace/tenant. - :param provider_name: The name of the API tool provider. - :param icon: The icon configuration for the provider. - :param credentials: The credentials for the provider. - :param schema_type: The type of schema (e.g., OpenAPI). - :param schema: The raw schema string. - :param privacy_policy: The privacy policy URL or text. - :param custom_disclaimer: Custom disclaimer text. - :param labels: A list of labels for the provider. - :return: A dictionary indicating the result status. + create api tool provider """ - provider_name = provider_name.strip() # check if the provider exists - # Create new session with automatic transaction management - with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: - provider: ApiToolProvider | None = _session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, - ) - .limit(1) + provider = db.session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, ) + .limit(1) + ) - if provider is not None: - raise ValueError(f"provider {provider_name} already exists") + if provider is not None: + raise ValueError(f"provider {provider_name} already exists") - # parse openapi to tool bundle - extra_info: dict[str, str] = {} - # extra info like description will be set here - tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) + # parse openapi to tool bundle + extra_info: dict[str, str] = {} + # extra info like description will be set here + tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) - if len(tool_bundles) > 100: - raise ValueError("the number of apis should be less than 100") + if len(tool_bundles) > 100: + raise ValueError("the number of apis should be less than 100") - # create API tool provider - api_tool_provider = ApiToolProvider( - tenant_id=tenant_id, - user_id=user_id, - name=provider_name, - icon=json.dumps(icon), - schema=schema, - description=extra_info.get("description", ""), - schema_type_str=schema_type, - tools_str=json.dumps(jsonable_encoder(tool_bundles)), - credentials_str="{}", - privacy_policy=privacy_policy, - custom_disclaimer=custom_disclaimer, - ) + # create db provider + db_provider = ApiToolProvider( + tenant_id=tenant_id, + user_id=user_id, + name=provider_name, + icon=json.dumps(icon), + schema=schema, + description=extra_info.get("description", ""), + schema_type_str=schema_type, + tools_str=json.dumps(jsonable_encoder(tool_bundles)), + credentials_str="{}", + privacy_policy=privacy_policy, + custom_disclaimer=custom_disclaimer, + ) - if "auth_type" not in credentials: - raise ValueError("auth_type is required") + if "auth_type" not in credentials: + raise ValueError("auth_type is required") - # get auth type, none or api key - auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) + # get auth type, none or api key + auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) - # create provider entity - provider_controller = ApiToolProviderController.from_db(api_tool_provider, auth_type) - # load tools into provider entity - provider_controller.load_bundled_tools(tool_bundles) + # create provider entity + provider_controller = ApiToolProviderController.from_db(db_provider, auth_type) + # load tools into provider entity + provider_controller.load_bundled_tools(tool_bundles) - # encrypt credentials - encrypter, _ = create_tool_provider_encrypter( - tenant_id=tenant_id, - controller=provider_controller, - ) - api_tool_provider.credentials_str = json.dumps(encrypter.encrypt(credentials)) + # encrypt credentials + encrypter, _ = create_tool_provider_encrypter( + tenant_id=tenant_id, + controller=provider_controller, + ) + db_provider.credentials_str = json.dumps(encrypter.encrypt(credentials)) - _session.add(api_tool_provider) + db.session.add(db_provider) + db.session.commit() - # update labels - ToolLabelManager.update_tool_labels(provider_controller, labels, _session) + # update labels + ToolLabelManager.update_tool_labels(provider_controller, labels) return {"result": "success"} @@ -228,25 +212,16 @@ class ApiToolManageService: @staticmethod def list_api_tool_provider_tools(user_id: str, tenant_id: str, provider_name: str) -> list[ToolApiEntity]: """ - List tools provided by a specific API tool provider. - - :param user_id: The ID of the user requesting the list. - :param tenant_id: The ID of the workspace/tenant. - :param provider_name: The name of the API tool provider. - :return: A list of ToolApiEntity objects. + list api tool provider tools """ - - # create new session with automatic transaction management - provider: ApiToolProvider | None = None - with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: - provider = _session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, - ) - .limit(1) + provider: ApiToolProvider | None = db.session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, ) + .limit(1) + ) if provider is None: raise ValueError(f"you have not added provider {provider_name}") @@ -276,133 +251,103 @@ class ApiToolManageService: privacy_policy: str | None, custom_disclaimer: str, labels: list[str], - ) -> dict[str, Any]: + ): """ - Update an existing API tool provider. - - :param user_id: The ID of the user updating the provider. - :param tenant_id: The ID of the workspace/tenant. - :param provider_name: The new name of the API tool provider. - :param original_provider: The original name of the API tool provider. - :param icon: The icon configuration for the provider. - :param credentials: The credentials for the provider. - :param _schema_type: The type of schema (e.g., OpenAPI). - :param schema: The raw schema string. - :param privacy_policy: The privacy policy URL or text. - :param custom_disclaimer: Custom disclaimer text. - :param labels: A list of labels for the provider. - :return: A dictionary indicating the result status. + update api tool provider """ - provider_name = provider_name.strip() # check if the provider exists - # create new session with automatic transaction management - with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: - provider: ApiToolProvider | None = _session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == original_provider, - ) - .limit(1) + provider = db.session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == original_provider, ) + .limit(1) + ) - if provider is None: - raise ApiToolProviderNotFoundError(provider_name=original_provider, tenant_id=tenant_id) + if provider is None: + raise ValueError(f"api provider {provider_name} does not exists") + # parse openapi to tool bundle + extra_info: dict[str, str] = {} + # extra info like description will be set here + tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) - # parse openapi to tool bundle - extra_info: dict[str, str] = {} - # extra info like description will be set here - tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) + # update db provider + provider.name = provider_name + provider.icon = json.dumps(icon) + provider.schema = schema + provider.description = extra_info.get("description", "") + provider.schema_type_str = schema_type + provider.tools_str = json.dumps(jsonable_encoder(tool_bundles)) + provider.privacy_policy = privacy_policy + provider.custom_disclaimer = custom_disclaimer - # update db provider - provider.name = provider_name - provider.icon = json.dumps(icon) - provider.schema = schema - provider.description = extra_info.get("description", "") - provider.schema_type_str = schema_type - provider.tools_str = json.dumps(jsonable_encoder(tool_bundles)) - provider.privacy_policy = privacy_policy - provider.custom_disclaimer = custom_disclaimer + if "auth_type" not in credentials: + raise ValueError("auth_type is required") - if "auth_type" not in credentials: - raise ValueError("auth_type is required") + # get auth type, none or api key + auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) - # get auth type, none or api key - auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) + # create provider entity + provider_controller = ApiToolProviderController.from_db(provider, auth_type) + # load tools into provider entity + provider_controller.load_bundled_tools(tool_bundles) - # create provider entity - provider_controller = ApiToolProviderController.from_db(provider, auth_type) - # load tools into provider entity - provider_controller.load_bundled_tools(tool_bundles) + # get original credentials if exists + encrypter, cache = create_tool_provider_encrypter( + tenant_id=tenant_id, + controller=provider_controller, + ) - # get original credentials if exists - encrypter, cache = create_tool_provider_encrypter( - tenant_id=tenant_id, - controller=provider_controller, - ) + original_credentials = encrypter.decrypt(provider.credentials) + masked_credentials = encrypter.mask_plugin_credentials(original_credentials) + # check if the credential has changed, save the original credential + for name, value in credentials.items(): + if name in masked_credentials and value == masked_credentials[name]: + credentials[name] = original_credentials[name] - original_credentials = encrypter.decrypt(provider.credentials) - masked_credentials = encrypter.mask_plugin_credentials(original_credentials) + credentials = dict(encrypter.encrypt(credentials)) + provider.credentials_str = json.dumps(credentials) - # check if the credential has changed, save the original credential - for name, value in credentials.items(): - if name in masked_credentials and value == masked_credentials[name]: - credentials[name] = original_credentials[name] - - credentials = dict(encrypter.encrypt(credentials)) - provider.credentials_str = json.dumps(credentials) - - _session.add(provider) - - # update labels - ToolLabelManager.update_tool_labels(provider_controller, labels, _session) + db.session.add(provider) + db.session.commit() # delete cache cache.delete() + # update labels + ToolLabelManager.update_tool_labels(provider_controller, labels) + return {"result": "success"} @staticmethod def delete_api_tool_provider(user_id: str, tenant_id: str, provider_name: str): """ - Delete an API tool provider. - - :param user_id: The ID of the user performing the deletion operation. - :param tenant_id: The ID of the workspace/tenant where the provider belongs. - :param provider_name: The unique name of the API tool provider to be deleted. - :raises ValueError: If the specified provider does not exist in the tenant. - :return: A dictionary indicating the result status. + delete tool provider """ - - # create new session with automatic transaction management - with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: - provider: ApiToolProvider | None = _session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, - ) - .limit(1) + provider = db.session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, ) + .limit(1) + ) - if provider is None: - raise ValueError(f"you have not added provider {provider_name}") + if provider is None: + raise ValueError(f"you have not added provider {provider_name}") - _session.delete(provider) + db.session.delete(provider) + db.session.commit() return {"result": "success"} @staticmethod - def get_api_tool_provider(user_id: str, tenant_id: str, provider: str) -> dict[str, Any]: + def get_api_tool_provider(user_id: str, tenant_id: str, provider: str): """ - Get API tool provider details. - - :param user_id: The ID of the user requesting the provider. - :param tenant_id: The ID of the workspace/tenant. - :param provider: The name of the API tool provider. - :return: A dictionary containing the provider details. + get api tool provider """ return ToolManager.user_get_api_provider(provider=provider, tenant_id=tenant_id) @@ -415,20 +360,10 @@ class ApiToolManageService: parameters: dict[str, Any], schema_type: ApiProviderSchemaType, schema: str, - ) -> dict[str, Any]: + ): """ - Test an API tool before adding the API tool provider. - - :param tenant_id: The ID of the workspace/tenant. - :param provider_name: The name of the API tool provider. - :param tool_name: The name of the specific tool to test. - :param credentials: The credentials for the provider. - :param parameters: The parameters to pass to the tool. - :param schema_type: The type of schema (e.g., OpenAPI). - :param schema: The raw schema string. - :return: A dictionary containing the result or error message. + test api tool before adding api tool provider """ - if schema_type not in [member.value for member in ApiProviderSchemaType]: raise ValueError(f"invalid schema type {schema_type}") @@ -442,21 +377,18 @@ class ApiToolManageService: if tool_bundle is None: raise ValueError(f"invalid tool name {tool_name}") - # create new session with automatic transaction management to get the provider - provider: ApiToolProvider | None = None - with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: - provider = _session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, - ) - .limit(1) + db_provider = db.session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, ) + .limit(1) + ) - if provider is None: + if not db_provider: # create a fake db provider - provider = ApiToolProvider( + db_provider = ApiToolProvider( tenant_id="", user_id="", name="", @@ -475,12 +407,12 @@ class ApiToolManageService: auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) # create provider entity - provider_controller = ApiToolProviderController.from_db(provider, auth_type) + provider_controller = ApiToolProviderController.from_db(db_provider, auth_type) # load tools into provider entity provider_controller.load_bundled_tools(tool_bundles) # decrypt credentials - if provider.id: + if db_provider.id: encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, controller=provider_controller, @@ -511,21 +443,14 @@ class ApiToolManageService: @staticmethod def list_api_tools(tenant_id: str) -> list[ToolProviderApiEntity]: """ - List all API tools for a specific tenant. - - :param tenant_id: The ID of the workspace/tenant. - :return: A list of ToolProviderApiEntity objects. + list api tools """ # get all api providers - # create new session with automatic transaction management - providers: list[ApiToolProvider] = [] - with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: - providers = list( - _session.scalars(select(ApiToolProvider).where(ApiToolProvider.tenant_id == tenant_id)).all() - ) + db_providers = db.session.scalars(select(ApiToolProvider).where(ApiToolProvider.tenant_id == tenant_id)).all() result: list[ToolProviderApiEntity] = [] - for provider in providers: + + for provider in db_providers: # convert provider controller to user provider provider_controller = ToolTransformService.api_provider_to_controller(db_provider=provider) labels = ToolLabelManager.get_tool_labels(provider_controller) diff --git a/api/services/trigger/trigger_service.py b/api/services/trigger/trigger_service.py index 911331e357..5a5d13b96d 100644 --- a/api/services/trigger/trigger_service.py +++ b/api/services/trigger/trigger_service.py @@ -5,6 +5,7 @@ from collections.abc import Mapping from typing import Any from flask import Request, Response +from graphon.entities.graph_config import NodeConfigDict from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import sessionmaker @@ -20,7 +21,6 @@ from core.trigger.utils.encryption import create_trigger_provider_encrypter_for_ from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData from extensions.ext_database import db from extensions.ext_redis import redis_client -from graphon.entities.graph_config import NodeConfigDict from models.model import App from models.provider_ids import TriggerProviderID from models.trigger import TriggerSubscription, WorkflowPluginTrigger diff --git a/api/services/variable_truncator.py b/api/services/variable_truncator.py index 1529c2b98f..56880f84af 100644 --- a/api/services/variable_truncator.py +++ b/api/services/variable_truncator.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from typing import Any, overload -from configs import dify_config from graphon.file import File from graphon.nodes.variable_assigner.common.helpers import UpdatedVariable from graphon.variables.segments import ( @@ -22,6 +21,8 @@ from graphon.variables.segments import ( ) from graphon.variables.utils import dumps_with_segments +from configs import dify_config + _MAX_DEPTH = 100 diff --git a/api/services/vector_service.py b/api/services/vector_service.py index 58193d75a9..9827c8dfbc 100644 --- a/api/services/vector_service.py +++ b/api/services/vector_service.py @@ -1,5 +1,6 @@ import logging +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import delete, select from core.model_manager import ModelInstance, ModelManager @@ -12,7 +13,6 @@ from core.rag.index_processor.index_processor_base import BaseIndexProcessor from core.rag.index_processor.index_processor_factory import IndexProcessorFactory from core.rag.models.document import AttachmentDocument, Document from extensions.ext_database import db -from graphon.model_runtime.entities.model_entities import ModelType from models import UploadFile from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment, SegmentAttachmentBinding from models.dataset import Document as DatasetDocument diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index 5dedb9e372..8e92449c6e 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -1,6 +1,11 @@ import json from typing import Any, TypedDict +from graphon.file import FileUploadConfig +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes import BuiltinNodeTypes +from graphon.variables.input_entities import VariableEntity from sqlalchemy import select from core.app.app_config.entities import ( @@ -19,15 +24,10 @@ from core.prompt.simple_prompt_transform import SimplePromptTransform from core.prompt.utils.prompt_template_parser import PromptTemplateParser from events.app_event import app_was_created from extensions.ext_database import db -from graphon.file import FileUploadConfig -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes import BuiltinNodeTypes -from graphon.variables.input_entities import VariableEntity from models import Account from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint from models.model import App, AppMode, AppModelConfig, IconType -from models.workflow import Workflow, WorkflowType +from models.workflow import Workflow, WorkflowKind, WorkflowType class _NodeType(TypedDict): @@ -208,6 +208,7 @@ class WorkflowConverter: tenant_id=app_model.tenant_id, app_id=app_model.id, type=WorkflowType.from_app_mode(new_app_mode).value, + kind=WorkflowKind.STANDARD.value, version=Workflow.VERSION_DRAFT, graph=json.dumps(graph), features=json.dumps(features), diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index e7325f9c6a..8c0878aa79 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -7,6 +7,19 @@ from datetime import datetime from enum import StrEnum from typing import Any, ClassVar, NotRequired, TypedDict +from graphon.enums import NodeType +from graphon.file import File +from graphon.nodes import BuiltinNodeTypes +from graphon.nodes.variable_assigner.common.helpers import get_updated_variables +from graphon.variable_loader import VariableLoader +from graphon.variables import Segment, StringSegment, VariableBase +from graphon.variables.consts import SELECTORS_LENGTH +from graphon.variables.segments import ( + ArrayFileSegment, + FileSegment, +) +from graphon.variables.types import SegmentType +from graphon.variables.utils import dumps_with_segments from sqlalchemy import Engine, delete, orm, select from sqlalchemy.dialects.mysql import insert as mysql_insert from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -27,19 +40,6 @@ from core.workflow.variable_prefixes import ( from extensions.ext_storage import storage from factories.file_factory import StorageKeyLoader from factories.variable_factory import build_segment, segment_to_variable -from graphon.enums import NodeType -from graphon.file import File -from graphon.nodes import BuiltinNodeTypes -from graphon.nodes.variable_assigner.common.helpers import get_updated_variables -from graphon.variable_loader import VariableLoader -from graphon.variables import Segment, StringSegment, VariableBase -from graphon.variables.consts import SELECTORS_LENGTH -from graphon.variables.segments import ( - ArrayFileSegment, - FileSegment, -) -from graphon.variables.types import SegmentType -from graphon.variables.utils import dumps_with_segments from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 from models import Account, App, Conversation diff --git a/api/services/workflow_event_snapshot_service.py b/api/services/workflow_event_snapshot_service.py index 5fca444723..601e9261fc 100644 --- a/api/services/workflow_event_snapshot_service.py +++ b/api/services/workflow_event_snapshot_service.py @@ -9,6 +9,10 @@ from collections.abc import Generator, Mapping, Sequence from dataclasses import dataclass from typing import Any +from graphon.entities import WorkflowStartReason +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import desc, select from sqlalchemy.orm import Session, sessionmaker @@ -22,10 +26,6 @@ from core.app.entities.task_entities import ( WorkflowStartStreamResponse, ) from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext -from graphon.entities import WorkflowStartReason -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from models.model import AppMode, Message from models.workflow import WorkflowNodeExecutionTriggeredFrom, WorkflowRun from repositories.api_workflow_node_execution_repository import WorkflowNodeExecutionSnapshot diff --git a/api/services/workflow_restore.py b/api/services/workflow_restore.py index 083235d228..062c8c2a6b 100644 --- a/api/services/workflow_restore.py +++ b/api/services/workflow_restore.py @@ -41,6 +41,7 @@ def apply_published_workflow_snapshot_to_draft( tenant_id=tenant_id, app_id=app_id, type=workflow_type, + kind=source_workflow.kind_or_standard, version=Workflow.VERSION_DRAFT, graph=source_workflow.graph, features=source_workflow.serialized_features, @@ -51,6 +52,7 @@ def apply_published_workflow_snapshot_to_draft( draft_workflow.graph = source_workflow.graph draft_workflow.features = source_workflow.serialized_features + draft_workflow.kind = source_workflow.resolved_kind draft_workflow.updated_by = account.id draft_workflow.updated_at = updated_at_factory() draft_workflow.copy_serialized_variable_storage_from(source_workflow) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 95c931a46e..6c0719d6f7 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -70,7 +70,13 @@ from models import Account from models.human_input import HumanInputFormRecipient, RecipientType from models.model import App, AppMode from models.tools import WorkflowToolProvider -from models.workflow import Workflow, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom, WorkflowType +from models.workflow import ( + Workflow, + WorkflowKind, + WorkflowNodeExecutionModel, + WorkflowNodeExecutionTriggeredFrom, + WorkflowType, +) from repositories.factory import DifyAPIRepositoryFactory from services.billing_service import BillingService from services.errors.app import ( @@ -278,7 +284,7 @@ class WorkflowService: """ stmt = select(Workflow).where( Workflow.tenant_id == tenant_id, - Workflow.type == WorkflowType.EVALUATION, + Workflow.kind == WorkflowKind.EVALUATION.value, Workflow.version != Workflow.VERSION_DRAFT, ) @@ -347,6 +353,7 @@ class WorkflowService: tenant_id=app_model.tenant_id, app_id=app_model.id, type=WorkflowType.from_app_mode(app_model.mode).value, + kind=WorkflowKind.STANDARD.value, version=Workflow.VERSION_DRAFT, graph=json.dumps(graph), features=json.dumps(features), @@ -363,6 +370,8 @@ class WorkflowService: workflow.updated_at = naive_utc_now() workflow.environment_variables = environment_variables workflow.conversation_variables = conversation_variables + if workflow.resolved_kind == WorkflowKind.STANDARD: + workflow.kind = WorkflowKind.STANDARD # commit db session changes db.session.commit() @@ -568,7 +577,7 @@ class WorkflowService: """Publish draft workflow as an evaluation workflow version. Compared to standard publish: - - force published workflow type to ``evaluation``; + - set business kind to ``evaluation``; - reject graphs containing trigger or human-input nodes. """ draft_workflow_stmt = select(Workflow).where( @@ -593,7 +602,7 @@ class WorkflowService: workflow = Workflow.new( tenant_id=app_model.tenant_id, app_id=app_model.id, - type=WorkflowType.EVALUATION.value, + type=draft_workflow.type.value, version=Workflow.version_from_datetime(naive_utc_now()), graph=draft_workflow.graph, created_by=account.id, @@ -603,6 +612,7 @@ class WorkflowService: marked_comment=marked_comment, rag_pipeline_variables=draft_workflow.rag_pipeline_variables, features=draft_workflow.features, + kind=WorkflowKind.EVALUATION.value, ) session.add(workflow) @@ -618,17 +628,16 @@ class WorkflowService: *, session: Session, app_model: App, - target_type: WorkflowType, + target_type: WorkflowKind, account: Account, ) -> Workflow: """ - Convert a published workflow type in-place. + Convert a published workflow business kind in-place. This endpoint only supports conversion between standard workflow and evaluation workflow. """ - if target_type not in {WorkflowType.WORKFLOW, WorkflowType.EVALUATION}: - raise ValueError( - "target_type must be either 'workflow' or 'evaluation'") + if target_type not in {WorkflowKind.STANDARD, WorkflowKind.EVALUATION}: + raise ValueError("target_type must be either 'standard' or 'evaluation'") if not app_model.workflow_id: raise WorkflowNotFoundError("Published workflow not found") @@ -646,13 +655,13 @@ class WorkflowService: raise IsDraftWorkflowError( "Current effective workflow cannot be a draft version.") - if workflow.type == target_type: + if workflow.resolved_kind == target_type: return workflow - if target_type == WorkflowType.EVALUATION: + if target_type == WorkflowKind.EVALUATION: self._validate_evaluation_workflow_nodes(workflow) - workflow.type = target_type + workflow.kind = target_type workflow.updated_by = account.id workflow.updated_at = naive_utc_now() @@ -1881,7 +1890,7 @@ def _setup_variable_pool( } # Only add chatflow-specific variables for chat-like workflow types. - if workflow.type not in {WorkflowType.WORKFLOW, WorkflowType.EVALUATION}: + if workflow.type != WorkflowType.WORKFLOW: system_variable_values.update( { "query": query, diff --git a/api/tasks/app_generate/workflow_execute_task.py b/api/tasks/app_generate/workflow_execute_task.py index c22e7e9918..8f2f5f261e 100644 --- a/api/tasks/app_generate/workflow_execute_task.py +++ b/api/tasks/app_generate/workflow_execute_task.py @@ -7,6 +7,7 @@ from typing import Annotated, Any from celery import shared_task from flask import current_app, json +from graphon.runtime import GraphRuntimeState from pydantic import BaseModel, Discriminator, Field, Tag from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker @@ -22,7 +23,6 @@ from core.app.entities.app_invoke_entities import ( from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig, WorkflowResumptionContext from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db -from graphon.runtime import GraphRuntimeState from libs.flask_utils import set_login_user from models.account import Account from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom diff --git a/api/tasks/batch_create_segment_to_index_task.py b/api/tasks/batch_create_segment_to_index_task.py index beb23d8354..4db551c73c 100644 --- a/api/tasks/batch_create_segment_to_index_task.py +++ b/api/tasks/batch_create_segment_to_index_task.py @@ -8,6 +8,7 @@ from typing import Any import click import pandas as pd from celery import shared_task +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import func, select from core.db.session_factory import session_factory @@ -15,7 +16,6 @@ from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from extensions.ext_redis import redis_client from extensions.ext_storage import storage -from graphon.model_runtime.entities.model_entities import ModelType from libs import helper from libs.datetime_utils import naive_utc_now from models.dataset import Dataset, Document, DocumentSegment diff --git a/api/tasks/human_input_timeout_tasks.py b/api/tasks/human_input_timeout_tasks.py index fd743205a1..ca73b4d374 100644 --- a/api/tasks/human_input_timeout_tasks.py +++ b/api/tasks/human_input_timeout_tasks.py @@ -2,6 +2,8 @@ import logging from datetime import timedelta from celery import shared_task +from graphon.enums import WorkflowExecutionStatus +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from sqlalchemy import or_, select from sqlalchemy.orm import sessionmaker @@ -9,8 +11,6 @@ from configs import dify_config from core.repositories.human_input_repository import HumanInputFormSubmissionRepository from extensions.ext_database import db from extensions.ext_storage import storage -from graphon.enums import WorkflowExecutionStatus -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import ensure_naive_utc, naive_utc_now from models.human_input import HumanInputForm from models.workflow import WorkflowPause, WorkflowRun diff --git a/api/tasks/mail_human_input_delivery_task.py b/api/tasks/mail_human_input_delivery_task.py index 2a60be7762..36c710d180 100644 --- a/api/tasks/mail_human_input_delivery_task.py +++ b/api/tasks/mail_human_input_delivery_task.py @@ -6,6 +6,7 @@ from typing import Any import click from celery import shared_task +from graphon.runtime import GraphRuntimeState, VariablePool from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -14,7 +15,6 @@ from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext from core.workflow.human_input_adapter import EmailDeliveryConfig, EmailDeliveryMethod from extensions.ext_database import db from extensions.ext_mail import mail -from graphon.runtime import GraphRuntimeState, VariablePool from models.human_input import ( DeliveryMethodType, HumanInputDelivery, diff --git a/api/tasks/workflow_execution_tasks.py b/api/tasks/workflow_execution_tasks.py index 5ca04fd7c2..b4f975f4da 100644 --- a/api/tasks/workflow_execution_tasks.py +++ b/api/tasks/workflow_execution_tasks.py @@ -10,11 +10,11 @@ import logging from typing import Any from celery import shared_task +from graphon.entities import WorkflowExecution +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import select from core.db.session_factory import session_factory -from graphon.entities import WorkflowExecution -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from models import CreatorUserRole, WorkflowRun from models.enums import WorkflowRunTriggeredFrom diff --git a/api/tasks/workflow_node_execution_tasks.py b/api/tasks/workflow_node_execution_tasks.py index 0d5475a56d..128cdd72e1 100644 --- a/api/tasks/workflow_node_execution_tasks.py +++ b/api/tasks/workflow_node_execution_tasks.py @@ -10,13 +10,13 @@ import logging from typing import Any from celery import shared_task -from sqlalchemy import select - -from core.db.session_factory import session_factory from graphon.entities.workflow_node_execution import ( WorkflowNodeExecution, ) from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter +from sqlalchemy import select + +from core.db.session_factory import session_factory from models import CreatorUserRole, WorkflowNodeExecutionModel from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py b/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py index a876b0c4aa..91245e879e 100644 --- a/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py +++ b/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py @@ -1,8 +1,9 @@ from collections.abc import Generator +from graphon.node_events import StreamCompletedEvent + from core.datasource.datasource_manager import DatasourceManager from core.datasource.entities.datasource_entities import DatasourceMessage -from graphon.node_events import StreamCompletedEvent def _gen_var_stream() -> Generator[DatasourceMessage, None, None]: diff --git a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py index c4146d5ccd..ce04a158a8 100644 --- a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py +++ b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py @@ -4,9 +4,6 @@ from collections.abc import Generator, Sequence from decimal import Decimal from json import dumps -from core.plugin.entities.plugin_daemon import PluginModelProviderEntity -from core.plugin.impl.model import PluginModelClient - # import monkeypatch from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.llm_entities import ( @@ -26,6 +23,9 @@ from graphon.model_runtime.entities.model_entities import ( ) from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity +from core.plugin.entities.plugin_daemon import PluginModelProviderEntity +from core.plugin.impl.model import PluginModelClient + class MockModelClass(PluginModelClient): def fetch_model_providers(self, tenant_id: str) -> Sequence[PluginModelProviderEntity]: diff --git a/api/tests/integration_tests/services/test_workflow_draft_variable_service.py b/api/tests/integration_tests/services/test_workflow_draft_variable_service.py index e130644338..c7bb90f019 100644 --- a/api/tests/integration_tests/services/test_workflow_draft_variable_service.py +++ b/api/tests/integration_tests/services/test_workflow_draft_variable_service.py @@ -3,6 +3,10 @@ import unittest import uuid import pytest +from graphon.nodes import BuiltinNodeTypes +from graphon.variables.segments import StringSegment +from graphon.variables.types import SegmentType +from graphon.variables.variables import StringVariable from sqlalchemy import delete, func, select from sqlalchemy.orm import Session @@ -11,10 +15,6 @@ from extensions.ext_database import db from extensions.ext_storage import storage from extensions.storage.storage_type import StorageType from factories.variable_factory import build_segment -from graphon.nodes import BuiltinNodeTypes -from graphon.variables.segments import StringSegment -from graphon.variables.types import SegmentType -from graphon.variables.variables import StringVariable from libs import datetime_utils from models.enums import CreatorUserRole from models.model import UploadFile diff --git a/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py b/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py index 4f444598b1..3dfedd811d 100644 --- a/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py +++ b/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py @@ -2,11 +2,11 @@ import uuid from unittest.mock import patch import pytest +from graphon.variables.segments import StringSegment from sqlalchemy import delete, func, select from core.db.session_factory import session_factory from extensions.storage.storage_type import StorageType -from graphon.variables.segments import StringSegment from models import Tenant from models.enums import CreatorUserRole from models.model import App, UploadFile @@ -209,6 +209,7 @@ class TestDeleteDraftVariablesWithOffloadIntegration: def setup_offload_test_data(self, app_and_tenant): tenant, app = app_and_tenant from graphon.variables.types import SegmentType + from libs.datetime_utils import naive_utc_now with session_factory.create_session() as session: @@ -452,6 +453,7 @@ class TestDeleteDraftVariablesSessionCommit: def setup_offload_test_data(self, app_and_tenant): """Create test data with offload files for session commit tests.""" from graphon.variables.types import SegmentType + from libs.datetime_utils import naive_utc_now tenant, app = app_and_tenant diff --git a/api/tests/integration_tests/workflow/nodes/__mock/model.py b/api/tests/integration_tests/workflow/nodes/__mock/model.py index a9a2617bae..c0143faa85 100644 --- a/api/tests/integration_tests/workflow/nodes/__mock/model.py +++ b/api/tests/integration_tests/workflow/nodes/__mock/model.py @@ -1,11 +1,12 @@ from unittest.mock import MagicMock +from graphon.model_runtime.entities.model_entities import ModelType + from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle from core.entities.provider_entities import CustomConfiguration, CustomProviderConfiguration, SystemConfiguration from core.model_manager import ModelInstance from core.plugin.impl.model_runtime_factory import create_plugin_model_provider_factory -from graphon.model_runtime.entities.model_entities import ModelType from models.provider import ProviderType diff --git a/api/tests/integration_tests/workflow/nodes/test_code.py b/api/tests/integration_tests/workflow/nodes/test_code.py index aaa6092993..bb0c585dc8 100644 --- a/api/tests/integration_tests/workflow/nodes/test_code.py +++ b/api/tests/integration_tests/workflow/nodes/test_code.py @@ -2,11 +2,6 @@ import time import uuid import pytest - -from configs import dify_config -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables from graphon.enums import WorkflowNodeExecutionStatus from graphon.graph import Graph from graphon.node_events import NodeRunResult @@ -14,6 +9,11 @@ from graphon.nodes.code.code_node import CodeNode from graphon.nodes.code.entities import CodeNodeData from graphon.nodes.code.limits import CodeNodeLimits from graphon.runtime import GraphRuntimeState, VariablePool + +from configs import dify_config +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.code_executor",) diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 3eead70163..bf640c7b96 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -4,11 +4,6 @@ import uuid from collections.abc import Generator from unittest.mock import MagicMock, patch -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.llm_generator.output_parser.structured_output import _parse_structured_output -from core.model_manager import ModelInstance -from core.workflow.system_variables import build_system_variables -from extensions.ext_database import db from graphon.enums import WorkflowNodeExecutionStatus from graphon.node_events import StreamCompletedEvent from graphon.nodes.llm.entities import LLMNodeData @@ -18,6 +13,12 @@ from graphon.nodes.llm.protocols import CredentialsProvider, ModelFactory from graphon.nodes.llm.runtime_protocols import PromptMessageSerializerProtocol from graphon.nodes.protocols import HttpClientProtocol from graphon.runtime import GraphRuntimeState, VariablePool + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.llm_generator.output_parser.structured_output import _parse_structured_output +from core.model_manager import ModelInstance +from core.workflow.system_variables import build_system_variables +from extensions.ext_database import db from tests.workflow_test_utils import build_test_graph_init_params """FOR MOCK FIXTURES, DO NOT REMOVE""" diff --git a/api/tests/integration_tests/workflow/nodes/test_template_transform.py b/api/tests/integration_tests/workflow/nodes/test_template_transform.py index e2e0723fb8..f87fbaf7ae 100644 --- a/api/tests/integration_tests/workflow/nodes/test_template_transform.py +++ b/api/tests/integration_tests/workflow/nodes/test_template_transform.py @@ -1,15 +1,16 @@ import time import uuid -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables from graphon.enums import WorkflowNodeExecutionStatus from graphon.graph import Graph from graphon.nodes.template_transform.entities import TemplateTransformNodeData from graphon.nodes.template_transform.template_transform_node import TemplateTransformNode from graphon.runtime import GraphRuntimeState, VariablePool from graphon.template_rendering import TemplateRenderError + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py index 5a22f81a69..ea95959a82 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py @@ -4,11 +4,11 @@ import json import uuid from flask.testing import FlaskClient +from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import Session from configs import dify_config from constants import HEADER_NAME_CSRF_TOKEN -from graphon.enums import WorkflowExecutionStatus from libs.datetime_utils import naive_utc_now from libs.token import _real_cookie_name, generate_csrf_token from models import Account, DifySetup, Tenant, TenantAccountJoin diff --git a/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py b/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py index c342e8994b..b4b65abdb6 100644 --- a/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py +++ b/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py @@ -22,6 +22,13 @@ import uuid from time import time import pytest +from graphon.entities.pause_reason import SchedulingPause +from graphon.enums import WorkflowExecutionStatus +from graphon.graph_engine.entities.commands import GraphEngineCommand +from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError +from graphon.graph_events import GraphRunPausedEvent +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool from sqlalchemy import Engine, delete, select from sqlalchemy.orm import Session @@ -33,13 +40,6 @@ from core.app.layers.pause_state_persist_layer import ( ) from core.workflow.system_variables import build_system_variables from extensions.ext_storage import storage -from graphon.entities.pause_reason import SchedulingPause -from graphon.enums import WorkflowExecutionStatus -from graphon.graph_engine.entities.commands import GraphEngineCommand -from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError -from graphon.graph_events import GraphRunPausedEvent -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool from libs.datetime_utils import naive_utc_now from models import Account from models import WorkflowPause as WorkflowPauseModel diff --git a/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py index 6524d6ce61..e0e1a07089 100644 --- a/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py @@ -4,6 +4,7 @@ from __future__ import annotations from uuid import uuid4 +from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData, UserAction from sqlalchemy import Engine, select from sqlalchemy.orm import Session @@ -17,7 +18,6 @@ from core.workflow.human_input_adapter import ( MemberRecipient, WebAppDeliveryMethod, ) -from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData, UserAction from models.account import ( Account, AccountStatus, diff --git a/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py b/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py index 5aed230cd4..241b638791 100644 --- a/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py +++ b/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py @@ -4,17 +4,6 @@ from datetime import timedelta from unittest.mock import MagicMock import pytest -from sqlalchemy import delete, select -from sqlalchemy.orm import Session - -from core.app.app_config.entities import WorkflowUIBasedAppConfig -from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity -from core.app.workflow.layers import PersistenceWorkflowInfo, WorkflowPersistenceLayer -from core.repositories.human_input_repository import HumanInputFormEntity, HumanInputFormRepository -from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository -from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository -from core.workflow.node_runtime import DifyHumanInputNodeRuntime -from core.workflow.system_variables import build_system_variables from graphon.enums import WorkflowType from graphon.graph import Graph from graphon.graph_engine import GraphEngine @@ -27,6 +16,17 @@ from graphon.nodes.human_input.human_input_node import HumanInputNode from graphon.nodes.start.entities import StartNodeData from graphon.nodes.start.start_node import StartNode from graphon.runtime import GraphRuntimeState, VariablePool +from sqlalchemy import delete, select +from sqlalchemy.orm import Session + +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity +from core.app.workflow.layers import PersistenceWorkflowInfo, WorkflowPersistenceLayer +from core.repositories.human_input_repository import HumanInputFormEntity, HumanInputFormRepository +from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository +from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository +from core.workflow.node_runtime import DifyHumanInputNodeRuntime +from core.workflow.system_variables import build_system_variables from libs.datetime_utils import naive_utc_now from models import Account from models.account import AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus diff --git a/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py b/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py index 35e41035df..066b149b37 100644 --- a/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py +++ b/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py @@ -4,13 +4,13 @@ from unittest.mock import patch from uuid import uuid4 import pytest +from graphon.file import File, FileTransferMethod, FileType from sqlalchemy.orm import Session from core.app.file_access import DatabaseFileAccessController from extensions.ext_database import db from extensions.storage.storage_type import StorageType from factories.file_factory import StorageKeyLoader -from graphon.file import File, FileTransferMethod, FileType from models import ToolFile, UploadFile from models.enums import CreatorUserRole diff --git a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py index 641399c7f9..a68b3a08c7 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py @@ -5,10 +5,10 @@ from __future__ import annotations from datetime import timedelta from uuid import uuid4 +from graphon.enums import WorkflowNodeExecutionStatus from sqlalchemy import Engine, delete from sqlalchemy.orm import Session, sessionmaker -from graphon.enums import WorkflowNodeExecutionStatus from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel diff --git a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py index aebe87839c..64c93ac07c 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py @@ -8,15 +8,15 @@ from unittest.mock import Mock from uuid import uuid4 import pytest -from sqlalchemy import Engine, delete, select -from sqlalchemy.orm import Session, sessionmaker - -from extensions.ext_storage import storage from graphon.entities import WorkflowExecution from graphon.entities.pause_reason import HumanInputRequired, PauseReasonType from graphon.enums import WorkflowExecutionStatus from graphon.nodes.human_input.entities import FormDefinition, FormInput, UserAction from graphon.nodes.human_input.enums import FormInputType, HumanInputFormStatus +from sqlalchemy import Engine, delete, select +from sqlalchemy.orm import Session, sessionmaker + +from extensions.ext_storage import storage from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.human_input import ( diff --git a/api/tests/test_containers_integration_tests/services/test_agent_service.py b/api/tests/test_containers_integration_tests/services/test_agent_service.py index 00a2f9a59f..4f3c0e4200 100644 --- a/api/tests/test_containers_integration_tests/services/test_agent_service.py +++ b/api/tests/test_containers_integration_tests/services/test_agent_service.py @@ -842,6 +842,7 @@ class TestAgentService: conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account) from graphon.file import FileTransferMethod, FileType + from models.enums import CreatorUserRole # Add files to message diff --git a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py index 77ce28b999..6c15587058 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py @@ -9,6 +9,7 @@ from uuid import uuid4 import pytest import yaml from faker import Faker +from graphon.enums import BuiltinNodeTypes from core.trigger.constants import ( TRIGGER_PLUGIN_NODE_TYPE, @@ -16,7 +17,6 @@ from core.trigger.constants import ( TRIGGER_WEBHOOK_NODE_TYPE, ) from extensions.ext_redis import redis_client -from graphon.enums import BuiltinNodeTypes from models import Account, AppMode from models.model import AppModelConfig, IconType from services import app_dsl_service diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service.py b/api/tests/test_containers_integration_tests/services/test_dataset_service.py index 0de3c64c4f..f9bfa570cb 100644 --- a/api/tests/test_containers_integration_tests/services/test_dataset_service.py +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service.py @@ -9,11 +9,11 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.retrieval.retrieval_methods import RetrievalMethod -from graphon.model_runtime.entities.model_entities import ModelType from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.dataset import Dataset, DatasetPermissionEnum, Document, ExternalKnowledgeBindings, Pipeline from models.enums import DatasetRuntimeMode, DataSourceType, DocumentCreatedFrom, IndexingStatus diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py b/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py index ac0483a45d..2974e00897 100644 --- a/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py @@ -3,10 +3,10 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexTechniqueType -from graphon.model_runtime.entities.model_entities import ModelType from models.account import ( Account, AccountStatus, diff --git a/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py b/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py index fe426ae516..c8f04e9215 100644 --- a/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py +++ b/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py @@ -5,9 +5,9 @@ Testcontainers integration tests for archived workflow run deletion service. from datetime import UTC, datetime, timedelta from uuid import uuid4 +from graphon.enums import WorkflowExecutionStatus from sqlalchemy import select -from graphon.enums import WorkflowExecutionStatus from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowArchiveLog, WorkflowRun from services.retention.workflow_run.delete_archived_workflow_run import ArchivedWorkflowRunDeletion diff --git a/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py b/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py index 80f9083e81..598daa86e6 100644 --- a/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py +++ b/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py @@ -3,6 +3,8 @@ import uuid from unittest.mock import MagicMock import pytest +from graphon.enums import BuiltinNodeTypes +from graphon.nodes.human_input.entities import HumanInputNodeData from core.workflow.human_input_adapter import ( EmailDeliveryConfig, @@ -10,8 +12,6 @@ from core.workflow.human_input_adapter import ( EmailRecipients, ExternalRecipient, ) -from graphon.enums import BuiltinNodeTypes -from graphon.nodes.human_input.entities import HumanInputNodeData from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.model import App, AppMode from models.workflow import Workflow, WorkflowType diff --git a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py index 8955a3b5f2..ba926bf675 100644 --- a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py +++ b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py @@ -2,10 +2,10 @@ from unittest.mock import MagicMock, patch import pytest from faker import Faker +from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType from sqlalchemy.orm import Session from core.entities.model_entities import ModelStatus -from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType from models import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.provider import Provider, ProviderModel, ProviderModelSetting, ProviderType from services.model_provider_service import ModelProviderService @@ -405,10 +405,11 @@ class TestModelProviderService: mock_provider_manager = mock_external_service_dependencies["provider_manager"].return_value # Create mock models - from core.entities.model_entities import ModelWithProviderEntity, SimpleModelProviderEntity from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.provider_entities import ProviderEntity + from core.entities.model_entities import ModelWithProviderEntity, SimpleModelProviderEntity + # Create real model objects instead of mocks provider_entity_1 = SimpleModelProviderEntity( ProviderEntity( @@ -643,9 +644,10 @@ class TestModelProviderService: mock_provider_manager = mock_external_service_dependencies["provider_manager"].return_value # Create mock default model response - from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity from graphon.model_runtime.entities.common_entities import I18nObject + from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity + mock_default_model = DefaultModelEntity( model="gpt-3.5-turbo", model_type=ModelType.LLM, diff --git a/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py index 1e57b5603d..749c6fff5b 100644 --- a/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py +++ b/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py @@ -8,9 +8,9 @@ from unittest.mock import patch import pytest from faker import Faker +from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import Session -from graphon.enums import WorkflowExecutionStatus from models import EndUser, Workflow, WorkflowAppLog, WorkflowArchiveLog, WorkflowRun from models.enums import AppTriggerType, CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowAppLogCreatedFrom diff --git a/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py index 86cf2327c7..0c281c8c33 100644 --- a/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py +++ b/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py @@ -1,9 +1,9 @@ import pytest from faker import Faker +from graphon.variables.segments import StringSegment from sqlalchemy.orm import Session from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID -from graphon.variables.segments import StringSegment from models import App, Workflow from models.enums import DraftVariableType from models.workflow import WorkflowDraftVariable diff --git a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py index 4dab895135..7c43bf676b 100644 --- a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py +++ b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py @@ -1,10 +1,10 @@ from datetime import datetime, timedelta from uuid import uuid4 +from graphon.enums import WorkflowNodeExecutionStatus from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker -from graphon.enums import WorkflowNodeExecutionStatus from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel diff --git a/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py b/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py index fa3ac12cf0..2fb62e0fc0 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py @@ -11,8 +11,7 @@ from unittest.mock import Mock, patch import pytest from faker import Faker -from sqlalchemy import ColumnElement, func, select -from sqlalchemy.orm import Session +from sqlalchemy import func, select from core.rag.index_processor.constant.index_type import IndexStructureType from models.dataset import Dataset, Document, DocumentSegment @@ -22,14 +21,6 @@ from tasks.clean_notion_document_task import clean_notion_document_task from tests.test_containers_integration_tests.helpers import generate_valid_password -def _count_documents(session: Session, condition: ColumnElement[bool]) -> int: - return session.scalar(select(func.count()).select_from(Document).where(condition)) or 0 - - -def _count_segments(session: Session, condition: ColumnElement[bool]) -> int: - return session.scalar(select(func.count()).select_from(DocumentSegment).where(condition)) or 0 - - class TestCleanNotionDocumentTask: """Integration tests for clean_notion_document_task using testcontainers.""" @@ -155,14 +146,29 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify data exists before cleanup - assert _count_documents(db_session_with_containers, Document.id.in_(document_ids)) == 3 - assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(document_ids)) == 6 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.id.in_(document_ids)) + ) + == 3 + ) + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids)) + ) + == 6 + ) # Execute cleanup task clean_notion_document_task(document_ids, dataset.id) # Verify segments are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(document_ids)) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids)) + ) + == 0 + ) # Verify index processor was called mock_processor = mock_index_processor_factory.return_value.init_index_processor.return_value @@ -322,7 +328,12 @@ class TestCleanNotionDocumentTask: # The task properly handles various index types and document configurations. # Verify segments are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) + ) + == 0 + ) # Reset mock for next iteration mock_index_processor_factory.reset_mock() @@ -405,7 +416,12 @@ class TestCleanNotionDocumentTask: clean_notion_document_task([document.id], dataset.id) # Verify segments are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) + ) + == 0 + ) # Note: This test successfully verifies that segments without index_node_ids # are properly deleted from the database. @@ -491,8 +507,18 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all data exists before cleanup - assert _count_documents(db_session_with_containers, Document.dataset_id == dataset.id) == 5 - assert _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == 10 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id) + ) + == 5 + ) + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) + ) + == 10 + ) # Clean up only first 3 documents documents_to_clean = [doc.id for doc in documents[:3]] @@ -502,12 +528,29 @@ class TestCleanNotionDocumentTask: clean_notion_document_task(documents_to_clean, dataset.id) # Verify only specified documents' segments are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(documents_to_clean)) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()) + .select_from(DocumentSegment) + .where(DocumentSegment.document_id.in_(documents_to_clean)) + ) + == 0 + ) # Verify remaining documents and segments are intact remaining_docs = [doc.id for doc in documents[3:]] - assert _count_documents(db_session_with_containers, Document.id.in_(remaining_docs)) == 2 - assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(remaining_docs)) == 4 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.id.in_(remaining_docs)) + ) + == 2 + ) + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(remaining_docs)) + ) + == 4 + ) # Note: This test successfully verifies partial document cleanup operations. # The database operations work correctly, isolating only the specified documents. @@ -591,13 +634,23 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all segments exist before cleanup - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 4 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) + ) + == 4 + ) # Execute cleanup task clean_notion_document_task([document.id], dataset.id) # Verify all segments are deleted regardless of status - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) + ) + == 0 + ) # Note: This test successfully verifies database operations. # IndexProcessor verification would require more sophisticated mocking. @@ -767,9 +820,16 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all data exists before cleanup - assert _count_documents(db_session_with_containers, Document.dataset_id == dataset.id) == num_documents assert ( - _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) + db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id) + ) + == num_documents + ) + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) + ) == num_documents * num_segments_per_doc ) @@ -778,7 +838,12 @@ class TestCleanNotionDocumentTask: clean_notion_document_task(all_document_ids, dataset.id) # Verify all segments are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) + ) + == 0 + ) # Note: This test successfully verifies bulk document cleanup operations. # The database efficiently handles large-scale deletions. @@ -885,12 +950,29 @@ class TestCleanNotionDocumentTask: clean_notion_document_task([target_document.id], target_dataset.id) # Verify only documents' segments from target dataset are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == target_document.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()) + .select_from(DocumentSegment) + .where(DocumentSegment.document_id == target_document.id) + ) + == 0 + ) # Verify documents from other datasets remain intact remaining_docs = [doc.id for doc in all_documents[1:]] - assert _count_documents(db_session_with_containers, Document.id.in_(remaining_docs)) == 2 - assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(remaining_docs)) == 6 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.id.in_(remaining_docs)) + ) + == 2 + ) + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(remaining_docs)) + ) + == 6 + ) # Note: This test successfully verifies multi-tenant isolation. # Only documents from the target dataset are affected, maintaining tenant separation. @@ -985,9 +1067,13 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all data exists before cleanup - assert _count_documents(db_session_with_containers, Document.dataset_id == dataset.id) == len(document_statuses) + assert db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id) + ) == len(document_statuses) assert ( - _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) + ) == len(document_statuses) * 2 ) @@ -996,7 +1082,12 @@ class TestCleanNotionDocumentTask: clean_notion_document_task(all_document_ids, dataset.id) # Verify all segments are deleted regardless of status - assert _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) + ) + == 0 + ) # Note: This test successfully verifies cleanup of documents in various states. # All documents are deleted regardless of their indexing status. @@ -1094,14 +1185,29 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify data exists before cleanup - assert _count_documents(db_session_with_containers, Document.id == document.id) == 1 - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 3 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(Document).where(Document.id == document.id) + ) + == 1 + ) + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) + ) + == 3 + ) # Execute cleanup task clean_notion_document_task([document.id], dataset.id) # Verify segments are deleted - assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 + assert ( + db_session_with_containers.scalar( + select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) + ) + == 0 + ) # Note: This test successfully verifies cleanup of documents with rich metadata. # The task properly handles complex document structures and metadata fields. diff --git a/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py b/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py index 95a867dbb5..5dcebef141 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py @@ -3,6 +3,9 @@ from datetime import UTC, datetime from unittest.mock import patch import pytest +from graphon.enums import WorkflowExecutionStatus +from graphon.nodes.human_input.entities import HumanInputNodeData +from graphon.runtime import GraphRuntimeState, VariablePool from sqlalchemy import delete from configs import dify_config @@ -18,9 +21,6 @@ from core.workflow.human_input_adapter import ( MemberRecipient, ) from extensions.ext_storage import storage -from graphon.enums import WorkflowExecutionStatus -from graphon.nodes.human_input.entities import HumanInputNodeData -from graphon.runtime import GraphRuntimeState, VariablePool from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.human_input import HumanInputDelivery, HumanInputForm, HumanInputFormRecipient diff --git a/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py b/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py index b43b622870..b5bef145d5 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py @@ -2,12 +2,12 @@ import uuid from unittest.mock import ANY, call, patch import pytest +from graphon.variables.segments import StringSegment +from graphon.variables.types import SegmentType from sqlalchemy import delete, func, select from core.db.session_factory import session_factory from extensions.storage.storage_type import StorageType -from graphon.variables.segments import StringSegment -from graphon.variables.types import SegmentType from libs.datetime_utils import naive_utc_now from models import Tenant from models.enums import CreatorUserRole diff --git a/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py b/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py index b00d827e37..6e98c0855a 100644 --- a/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py +++ b/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py @@ -24,12 +24,12 @@ from dataclasses import dataclass from datetime import timedelta import pytest +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowExecutionStatus from sqlalchemy import delete, func, select from sqlalchemy.orm import Session, selectinload, sessionmaker from extensions.ext_storage import storage -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowExecutionStatus from libs.datetime_utils import naive_utc_now from models import Account from models import WorkflowPause as WorkflowPauseModel diff --git a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py index 9c20118e27..7c4553d4a0 100644 --- a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py +++ b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py @@ -10,6 +10,7 @@ from typing import Any import pytest from flask import Flask, Response from flask.testing import FlaskClient +from graphon.enums import BuiltinNodeTypes from sqlalchemy import select from sqlalchemy.orm import Session @@ -24,7 +25,6 @@ from core.trigger.debug import event_selectors from core.trigger.debug.event_bus import TriggerDebugEventBus from core.trigger.debug.event_selectors import PluginTriggerDebugEventPoller, WebhookTriggerDebugEventPoller from core.trigger.debug.events import PluginTriggerDebugEvent, build_plugin_pool_key -from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now from models.account import Account, Tenant from models.enums import AppTriggerStatus, AppTriggerType, CreatorUserRole, WorkflowTriggerStatus diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py index c4a8148446..e11102acb1 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py @@ -6,14 +6,14 @@ from unittest.mock import Mock import pytest from flask import Flask - -from controllers.console import wraps as console_wraps -from controllers.console.app import workflow_run as workflow_run_module -from controllers.web.error import NotFoundError from graphon.entities.pause_reason import HumanInputRequired from graphon.enums import WorkflowExecutionStatus from graphon.nodes.human_input.entities import FormInput, UserAction from graphon.nodes.human_input.enums import FormInputType + +from controllers.console import wraps as console_wraps +from controllers.console.app import workflow_run as workflow_run_module +from controllers.web.error import NotFoundError from libs import login as login_lib from models.account import Account, AccountStatus, TenantAccountRole from models.workflow import WorkflowRun diff --git a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py index 22b80b748e..7b329e0c0d 100644 --- a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py +++ b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from flask_restx import marshal +from graphon.variables.types import SegmentType from controllers.console.app.workflow_draft_variable import ( _WORKFLOW_DRAFT_VARIABLE_FIELDS, @@ -15,7 +16,6 @@ from controllers.console.app.workflow_draft_variable import ( ) from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from factories.variable_factory import build_segment -from graphon.variables.types import SegmentType from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 from models.workflow import WorkflowDraftVariable, WorkflowDraftVariableFile diff --git a/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py b/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py index b2f949c6e2..9c42ee9529 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py @@ -11,10 +11,9 @@ from unittest.mock import MagicMock import pytest from flask import Flask from flask.views import MethodView -from werkzeug.exceptions import Forbidden - from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from werkzeug.exceptions import Forbidden if not hasattr(builtins, "MethodView"): builtins.MethodView = MethodView # type: ignore[attr-defined] diff --git a/api/tests/unit_tests/controllers/service_api/app/test_audio.py b/api/tests/unit_tests/controllers/service_api/app/test_audio.py index c16ebad739..a26fea8fbd 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_audio.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_audio.py @@ -13,6 +13,7 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest +from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.datastructures import FileStorage from werkzeug.exceptions import InternalServerError @@ -29,7 +30,6 @@ from controllers.service_api.app.error import ( UnsupportedAudioTypeError, ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError from services.audio_service import AudioService from services.errors.app_model_config import AppModelConfigBrokenError from services.errors.audio import ( diff --git a/api/tests/unit_tests/controllers/service_api/app/test_completion.py b/api/tests/unit_tests/controllers/service_api/app/test_completion.py index 3364c07e62..57681d8f5b 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_completion.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_completion.py @@ -16,6 +16,7 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest +from graphon.model_runtime.errors.invoke import InvokeError from pydantic import ValidationError from werkzeug.exceptions import BadRequest, NotFound @@ -34,7 +35,6 @@ from controllers.service_api.app.error import ( NotChatAppError, ) from core.errors.error import QuotaExceededError -from graphon.model_runtime.errors.invoke import InvokeError from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService from services.app_task_service import AppTaskService diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py index da09ec13ce..74a3c75839 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py @@ -20,6 +20,7 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest +from graphon.enums import WorkflowExecutionStatus from werkzeug.exceptions import BadRequest, NotFound from controllers.service_api.app.error import NotWorkflowAppError @@ -36,7 +37,6 @@ from controllers.service_api.app.workflow import ( WorkflowTaskStopApi, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError -from graphon.enums import WorkflowExecutionStatus from models.model import App, AppMode from services.app_generate_service import AppGenerateService from services.errors.app import IsDraftWorkflowError, WorkflowNotFoundError diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py index eda270258d..4b8e3a738c 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py @@ -1,8 +1,9 @@ from types import SimpleNamespace -from controllers.service_api.app.workflow import WorkflowRunOutputsField, WorkflowRunStatusField from graphon.enums import WorkflowExecutionStatus +from controllers.service_api.app.workflow import WorkflowRunOutputsField, WorkflowRunStatusField + def test_workflow_run_status_field_with_enum() -> None: field = WorkflowRunStatusField() diff --git a/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py b/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py index 11b53dd0f9..8bde9c1f97 100644 --- a/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py +++ b/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py @@ -1,7 +1,8 @@ -from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from graphon.file import FileTransferMethod, FileUploadConfig, ImageConfig from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager + def test_convert_with_vision(): config = { diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py index 45d4b0e321..1fb0dc6cf1 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py @@ -3,12 +3,12 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 +from graphon.variables import SegmentType from sqlalchemy.orm import Session from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom from factories import variable_factory -from graphon.variables import SegmentType from models import ConversationVariable, Workflow MINIMAL_GRAPH = { diff --git a/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py b/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py index b3ea1a464f..f255d2c7df 100644 --- a/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py +++ b/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py @@ -4,13 +4,13 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest +from graphon.file import FileTransferMethod, FileType +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from core.app.apps.base_app_queue_manager import PublishFrom from core.app.apps.base_app_runner import AppRunner from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueMessageFileEvent -from graphon.file import FileTransferMethod, FileType -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.enums import CreatorUserRole diff --git a/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py b/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py index 201923e0e4..4a94a2b4f1 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py +++ b/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py @@ -1,11 +1,11 @@ from types import SimpleNamespace import pytest +from graphon.runtime import GraphRuntimeState, VariablePool from core.app.apps.common.graph_runtime_state_support import GraphRuntimeStateSupport from core.workflow.system_variables import build_system_variables from core.workflow.variable_pool_initializer import add_variables_to_pool -from graphon.runtime import GraphRuntimeState, VariablePool def _make_state(workflow_run_id: str | None) -> GraphRuntimeState: diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py index dd6cd0e919..82fcccdf40 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py @@ -1,9 +1,10 @@ from collections.abc import Mapping, Sequence -from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType from graphon.variables.segments import ArrayFileSegment, FileSegment +from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter + class TestWorkflowResponseConverterFetchFilesFromVariableValue: """Test class for WorkflowResponseConverter._fetch_files_from_variable_value method""" diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py index 1bef6f69cd..bc11bf4174 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py @@ -1,12 +1,13 @@ from datetime import UTC, datetime from types import SimpleNamespace +from graphon.entities import WorkflowStartReason +from graphon.runtime import GraphRuntimeState, VariablePool + from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueHumanInputFormFilledEvent, QueueHumanInputFormTimeoutEvent from core.workflow.system_variables import build_system_variables -from graphon.entities import WorkflowStartReason -from graphon.runtime import GraphRuntimeState, VariablePool def _build_converter(): diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py index 936ac37e55..c9e146ff12 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py @@ -1,10 +1,11 @@ from types import SimpleNamespace +from graphon.entities import WorkflowStartReason +from graphon.runtime import GraphRuntimeState, VariablePool + from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.system_variables import build_system_variables -from graphon.entities import WorkflowStartReason -from graphon.runtime import GraphRuntimeState, VariablePool def _build_converter() -> WorkflowResponseConverter: diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py index b3c0eb74fa..0fde7565d2 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py @@ -10,6 +10,8 @@ from typing import Any from unittest.mock import Mock import pytest +from graphon.entities import WorkflowStartReason +from graphon.enums import BuiltinNodeTypes from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter @@ -25,8 +27,6 @@ from core.app.entities.queue_entities import ( QueueNodeSucceededEvent, ) from core.workflow.system_variables import build_system_variables -from graphon.entities import WorkflowStartReason -from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now from models import Account from models.model import AppMode diff --git a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py index b0f8b423e1..6167be3bbd 100644 --- a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py @@ -1,7 +1,7 @@ import pytest +from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.app.apps.base_app_generator import BaseAppGenerator -from graphon.variables.input_entities import VariableEntity, VariableEntityType def test_validate_inputs_with_zero(): @@ -476,8 +476,9 @@ class TestBaseAppGeneratorExtras: assert converted[1] == "event: ping\n\n" def test_get_draft_var_saver_factory_debugger(self): - from core.app.entities.app_invoke_entities import InvokeFrom from graphon.enums import BuiltinNodeTypes + + from core.app.entities.app_invoke_entities import InvokeFrom from models import Account base_app_generator = BaseAppGenerator() diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py index 10fb2271f4..aa789d9ff3 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock import pytest +from graphon.entities.pause_reason import HumanInputRequired +from graphon.graph_events import GraphRunPausedEvent from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner from core.app.entities.queue_entities import QueueWorkflowPausedEvent -from graphon.entities.pause_reason import HumanInputRequired -from graphon.graph_events import GraphRunPausedEvent class _DummyQueueManager: diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py index 620a153204..9e30faecf2 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py @@ -4,14 +4,14 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.runtime import GraphRuntimeState, VariablePool from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow.app_runner import WorkflowAppRunner from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.workflow.system_variables import default_system_variables -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.runtime import GraphRuntimeState, VariablePool from models.workflow import Workflow diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py b/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py index a3ab379b66..8a717e1dcc 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py @@ -3,6 +3,11 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest +from graphon.entities import WorkflowStartReason +from graphon.entities.pause_reason import HumanInputRequired +from graphon.graph_events import GraphRunPausedEvent +from graphon.nodes.human_input.entities import FormInput, UserAction +from graphon.nodes.human_input.enums import FormInputType from core.app.apps.common import workflow_response_converter from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter @@ -11,11 +16,6 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueWorkflowPausedEvent from core.app.entities.task_entities import HumanInputRequiredResponse, WorkflowPauseStreamResponse from core.workflow.system_variables import build_system_variables -from graphon.entities import WorkflowStartReason -from graphon.entities.pause_reason import HumanInputRequired -from graphon.graph_events import GraphRunPausedEvent -from graphon.nodes.human_input.entities import FormInput, UserAction -from graphon.nodes.human_input.enums import FormInputType from models.account import Account from models.human_input import RecipientType diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py index 1f6e7e12ef..29df903aa8 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py @@ -2,14 +2,15 @@ import time from contextlib import contextmanager from unittest.mock import MagicMock +from graphon.entities import WorkflowStartReason +from graphon.runtime import GraphRuntimeState + from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow.generate_task_pipeline import WorkflowAppGenerateTaskPipeline from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.queue_entities import QueueWorkflowStartedEvent from core.workflow.system_variables import build_system_variables -from graphon.entities import WorkflowStartReason -from graphon.runtime import GraphRuntimeState from models.account import Account from models.model import AppMode from tests.workflow_test_utils import build_test_variable_pool diff --git a/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py b/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py index ba55e8f695..a78c1b428f 100644 --- a/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py @@ -1,9 +1,6 @@ from collections.abc import Sequence from unittest.mock import Mock -from core.app.layers.conversation_variable_persist_layer import ConversationVariablePersistenceLayer -from core.workflow.system_variables import SystemVariableKey -from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.graph_engine.command_channels import CommandChannel from graphon.graph_events import NodeRunSucceededEvent, NodeRunVariableUpdatedEvent @@ -11,6 +8,10 @@ from graphon.node_events import NodeRunResult from graphon.runtime import ReadOnlyGraphRuntimeState from graphon.variables import StringVariable from graphon.variables.segments import Segment, StringSegment + +from core.app.layers.conversation_variable_persist_layer import ConversationVariablePersistenceLayer +from core.workflow.system_variables import SystemVariableKey +from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID from libs.datetime_utils import naive_utc_now diff --git a/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py b/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py index 539944d683..035e64325b 100644 --- a/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py @@ -4,16 +4,6 @@ from time import time from unittest.mock import Mock import pytest - -from core.app.app_config.entities import WorkflowUIBasedAppConfig -from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity -from core.app.layers.pause_state_persist_layer import ( - PauseStatePersistenceLayer, - WorkflowResumptionContext, - _AdvancedChatAppGenerateEntityWrapper, - _WorkflowGenerateEntityWrapper, -) -from core.workflow.system_variables import SystemVariableKey from graphon.entities.pause_reason import SchedulingPause from graphon.graph_engine.entities.commands import GraphEngineCommand from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError @@ -25,6 +15,16 @@ from graphon.graph_events import ( ) from graphon.runtime import ReadOnlyVariablePool from graphon.variables.segments import Segment + +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity +from core.app.layers.pause_state_persist_layer import ( + PauseStatePersistenceLayer, + WorkflowResumptionContext, + _AdvancedChatAppGenerateEntityWrapper, + _WorkflowGenerateEntityWrapper, +) +from core.workflow.system_variables import SystemVariableKey from models.model import AppMode from repositories.factory import DifyAPIRepositoryFactory diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py index 1c1bf391d3..4aaa10a81a 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py @@ -2,6 +2,8 @@ from types import SimpleNamespace from unittest.mock import ANY, Mock, patch import pytest +from graphon.model_runtime.entities.llm_entities import LLMResult as RuntimeLLMResult +from graphon.model_runtime.entities.message_entities import TextPromptMessageContent from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import ChatAppGenerateEntity @@ -26,8 +28,6 @@ from core.app.entities.task_entities import ( from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBasedGenerateTaskPipeline from core.base.tts import AppGeneratorTTSPublisher from core.ops.ops_trace_manager import TraceQueueManager -from graphon.model_runtime.entities.llm_entities import LLMResult as RuntimeLLMResult -from graphon.model_runtime.entities.message_entities import TextPromptMessageContent from models.model import AppMode diff --git a/api/tests/unit_tests/core/datasource/test_datasource_manager.py b/api/tests/unit_tests/core/datasource/test_datasource_manager.py index deeac49bbc..af54aed363 100644 --- a/api/tests/unit_tests/core/datasource/test_datasource_manager.py +++ b/api/tests/unit_tests/core/datasource/test_datasource_manager.py @@ -2,15 +2,15 @@ import types from collections.abc import Generator import pytest +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.file import File, FileTransferMethod, FileType +from graphon.node_events import StreamChunkEvent, StreamCompletedEvent from contexts.wrapper import RecyclableContextVar from core.datasource.datasource_manager import DatasourceManager from core.datasource.entities.datasource_entities import DatasourceMessage, DatasourceProviderType from core.datasource.errors import DatasourceProviderNotFoundError from core.workflow.file_reference import parse_file_reference -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.file import File, FileTransferMethod, FileType -from graphon.node_events import StreamChunkEvent, StreamCompletedEvent def _gen_messages_text_only(text: str) -> Generator[DatasourceMessage, None, None]: diff --git a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py index 57456085c3..9a815fb94d 100644 --- a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py +++ b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch import jsonschema import pytest +from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.mcp import types @@ -18,7 +19,6 @@ from core.mcp.server.streamable_http import ( prepare_tool_arguments, process_mapping_response, ) -from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import App, AppMCPServer, AppMode, EndUser diff --git a/api/tests/unit_tests/core/plugin/test_plugin_runtime.py b/api/tests/unit_tests/core/plugin/test_plugin_runtime.py index 704b82adc0..a3b1e5f6b0 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_runtime.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_runtime.py @@ -17,6 +17,14 @@ from unittest.mock import MagicMock, patch import httpx import pytest +from graphon.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic import BaseModel from core.plugin.entities.plugin_daemon import ( @@ -37,14 +45,6 @@ from core.plugin.impl.exc import ( ) from core.plugin.impl.plugin import PluginInstaller from core.plugin.impl.tool import PluginToolManager -from graphon.model_runtime.errors.invoke import ( - InvokeAuthorizationError, - InvokeBadRequestError, - InvokeConnectionError, - InvokeRateLimitError, - InvokeServerUnavailableError, -) -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError @pytest.fixture(autouse=True) diff --git a/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py b/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py index 00a4207786..014853e480 100644 --- a/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py +++ b/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py @@ -1,12 +1,12 @@ from collections.abc import Generator import pytest +from graphon.file import File, FileTransferMethod, FileType from core.agent.entities import AgentInvokeMessage from core.plugin.utils.chunk_merger import FileChunk, merge_blob_chunks from core.plugin.utils.converter import convert_parameters_to_plugin_format from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter, ToolSelector -from graphon.file import File, FileTransferMethod, FileType class TestChunkMerger: diff --git a/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py index e536c0831f..6fee5790cc 100644 --- a/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py @@ -2,13 +2,6 @@ from typing import cast from unittest.mock import MagicMock, patch import pytest - -from configs import dify_config -from core.app.app_config.entities import ModelConfigEntity -from core.memory.token_buffer_memory import TokenBufferMemory -from core.prompt.advanced_prompt_transform import AdvancedPromptTransform -from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig -from core.prompt.utils.prompt_template_parser import PromptTemplateParser from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, @@ -18,6 +11,13 @@ from graphon.model_runtime.entities.message_entities import ( TextPromptMessageContent, UserPromptMessage, ) + +from configs import dify_config +from core.app.app_config.entities import ModelConfigEntity +from core.memory.token_buffer_memory import TokenBufferMemory +from core.prompt.advanced_prompt_transform import AdvancedPromptTransform +from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig +from core.prompt.utils.prompt_template_parser import PromptTemplateParser from models.model import Conversation diff --git a/api/tests/unit_tests/core/prompt/test_prompt_message.py b/api/tests/unit_tests/core/prompt/test_prompt_message.py index 5d865d934c..a4b3960b0a 100644 --- a/api/tests/unit_tests/core/prompt/test_prompt_message.py +++ b/api/tests/unit_tests/core/prompt/test_prompt_message.py @@ -1,5 +1,3 @@ -from core.prompt.simple_prompt_transform import ModelMode -from core.prompt.utils.prompt_message_util import PromptMessageUtil from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, AudioPromptMessageContent, @@ -9,6 +7,9 @@ from graphon.model_runtime.entities.message_entities import ( UserPromptMessage, ) +from core.prompt.simple_prompt_transform import ModelMode +from core.prompt.utils.prompt_message_util import PromptMessageUtil + def test_build_prompt_message_with_prompt_message_contents(): prompt = UserPromptMessage(content=[TextPromptMessageContent(data="Hello, World!")]) diff --git a/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py b/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py index 4b8175b0b4..408cf14a51 100644 --- a/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py +++ b/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py @@ -49,10 +49,6 @@ from unittest.mock import Mock, patch import numpy as np import pytest -from sqlalchemy.exc import IntegrityError - -from core.entities.embedding_type import EmbeddingInputType -from core.rag.embedding.cached_embedding import CacheEmbedding from graphon.model_runtime.entities.model_entities import ModelPropertyKey from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult, EmbeddingUsage from graphon.model_runtime.errors.invoke import ( @@ -60,6 +56,10 @@ from graphon.model_runtime.errors.invoke import ( InvokeConnectionError, InvokeRateLimitError, ) +from sqlalchemy.exc import IntegrityError + +from core.entities.embedding_type import EmbeddingInputType +from core.rag.embedding.cached_embedding import CacheEmbedding from models.dataset import Embedding diff --git a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py index 7c4defc180..641c5d9ba0 100644 --- a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py +++ b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py @@ -53,6 +53,7 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy.orm.exc import ObjectDeletedError from core.errors.error import ProviderTokenNotInitError @@ -63,7 +64,6 @@ from core.indexing_runner import ( ) from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.models.document import ChildDocument, Document -from graphon.model_runtime.entities.model_entities import ModelType from libs.datetime_utils import naive_utc_now from models.dataset import Dataset, DatasetProcessRule from models.dataset import Document as DatasetDocument diff --git a/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py b/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py index 3d3322094e..e229d5fc1a 100644 --- a/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py @@ -9,10 +9,10 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest - -from core.repositories.celery_workflow_execution_repository import CeleryWorkflowExecutionRepository from graphon.entities import WorkflowExecution from graphon.enums import WorkflowType + +from core.repositories.celery_workflow_execution_repository import CeleryWorkflowExecutionRepository from libs.datetime_utils import naive_utc_now from models import Account, EndUser from models.enums import WorkflowRunTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py b/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py index 05b4f3a053..7dbf78d0f0 100644 --- a/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py @@ -9,14 +9,14 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest - -from core.repositories.celery_workflow_node_execution_repository import CeleryWorkflowNodeExecutionRepository -from core.repositories.factory import OrderConfig from graphon.entities.workflow_node_execution import ( WorkflowNodeExecution, WorkflowNodeExecutionStatus, ) from graphon.enums import BuiltinNodeTypes + +from core.repositories.celery_workflow_node_execution_repository import CeleryWorkflowNodeExecutionRepository +from core.repositories.factory import OrderConfig from libs.datetime_utils import naive_utc_now from models import Account, EndUser from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py index 18ae9fafc8..5bbcd2202d 100644 --- a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py @@ -7,6 +7,11 @@ from datetime import datetime from types import SimpleNamespace import pytest +from graphon.nodes.human_input.entities import ( + FormDefinition, + UserAction, +) +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from core.repositories.human_input_repository import ( HumanInputFormRecord, @@ -21,11 +26,6 @@ from core.workflow.human_input_adapter import ( ExternalRecipient, MemberRecipient, ) -from graphon.nodes.human_input.entities import ( - FormDefinition, - UserAction, -) -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import naive_utc_now from models.human_input import ( EmailExternalRecipientPayload, diff --git a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py index abdbc72085..84fe522388 100644 --- a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py +++ b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py @@ -4,17 +4,17 @@ from unittest.mock import MagicMock, Mock import psycopg2.errors import pytest +from graphon.entities.workflow_node_execution import ( + WorkflowNodeExecution, + WorkflowNodeExecutionStatus, +) +from graphon.enums import BuiltinNodeTypes from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker from core.repositories.sqlalchemy_workflow_node_execution_repository import ( SQLAlchemyWorkflowNodeExecutionRepository, ) -from graphon.entities.workflow_node_execution import ( - WorkflowNodeExecution, - WorkflowNodeExecutionStatus, -) -from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now from models import Account, WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py index 5af1376a0a..27729e7f06 100644 --- a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py +++ b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py @@ -11,17 +11,17 @@ from datetime import UTC, datetime from typing import Any from unittest.mock import MagicMock +from graphon.entities.workflow_node_execution import ( + WorkflowNodeExecution, + WorkflowNodeExecutionStatus, +) +from graphon.enums import BuiltinNodeTypes from sqlalchemy import Engine from configs import dify_config from core.repositories.sqlalchemy_workflow_node_execution_repository import ( SQLAlchemyWorkflowNodeExecutionRepository, ) -from graphon.entities.workflow_node_execution import ( - WorkflowNodeExecution, - WorkflowNodeExecutionStatus, -) -from graphon.enums import BuiltinNodeTypes from models import Account, WorkflowNodeExecutionTriggeredFrom from models.enums import ExecutionOffLoadType from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload diff --git a/api/tests/unit_tests/core/test_file.py b/api/tests/unit_tests/core/test_file.py index eab0176f41..00a3a89c0b 100644 --- a/api/tests/unit_tests/core/test_file.py +++ b/api/tests/unit_tests/core/test_file.py @@ -1,6 +1,7 @@ import json from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig + from models.workflow import Workflow diff --git a/api/tests/unit_tests/core/test_model_manager.py b/api/tests/unit_tests/core/test_model_manager.py index afea9144c0..f5efb78b61 100644 --- a/api/tests/unit_tests/core/test_model_manager.py +++ b/api/tests/unit_tests/core/test_model_manager.py @@ -2,12 +2,12 @@ from unittest.mock import MagicMock, patch import pytest import redis +from graphon.model_runtime.entities.model_entities import ModelType from pytest_mock import MockerFixture from core.entities.provider_entities import ModelLoadBalancingConfiguration from core.model_manager import LBModelManager from extensions.ext_redis import redis_client -from graphon.model_runtime.entities.model_entities import ModelType @pytest.fixture diff --git a/api/tests/unit_tests/core/test_provider_configuration.py b/api/tests/unit_tests/core/test_provider_configuration.py index b19a21d7f4..331166fe63 100644 --- a/api/tests/unit_tests/core/test_provider_configuration.py +++ b/api/tests/unit_tests/core/test_provider_configuration.py @@ -1,6 +1,15 @@ from unittest.mock import Mock, patch import pytest +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + CredentialFormSchema, + FormOption, + FormType, + ProviderEntity, +) from core.entities.provider_configuration import ProviderConfiguration, SystemConfigurationStatus from core.entities.provider_entities import ( @@ -12,15 +21,6 @@ from core.entities.provider_entities import ( RestrictModel, SystemConfiguration, ) -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - CredentialFormSchema, - FormOption, - FormType, - ProviderEntity, -) from models.provider import Provider, ProviderType diff --git a/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py b/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py index 43f3fbd5c9..0e3a7e623a 100644 --- a/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py +++ b/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py @@ -1,9 +1,9 @@ import pytest +from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.tools.entities.tool_entities import ToolParameter, WorkflowToolParameterConfiguration from core.tools.errors import WorkflowToolHumanInputNotSupportedError from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurationUtils -from graphon.variables.input_entities import VariableEntity, VariableEntityType def test_ensure_no_human_input_nodes_passes_for_non_human_input(): diff --git a/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py b/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py index 72a73dd936..c20edd7400 100644 --- a/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py +++ b/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py @@ -11,6 +11,7 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest +from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod, FileType from core.app.entities.app_invoke_entities import InvokeFrom from core.tools.__base.tool_runtime import ToolRuntime @@ -24,7 +25,6 @@ from core.tools.entities.tool_entities import ( ) from core.tools.errors import ToolInvokeError from core.tools.workflow_as_tool.tool import WorkflowTool -from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod, FileType class StubScalars: diff --git a/api/tests/unit_tests/core/variables/test_segment_type.py b/api/tests/unit_tests/core/variables/test_segment_type.py index d4e862220a..37ecd2890b 100644 --- a/api/tests/unit_tests/core/variables/test_segment_type.py +++ b/api/tests/unit_tests/core/variables/test_segment_type.py @@ -1,5 +1,4 @@ import pytest - from graphon.variables.segment_group import SegmentGroup from graphon.variables.segments import StringSegment from graphon.variables.types import ArrayValidation, SegmentType diff --git a/api/tests/unit_tests/core/variables/test_segment_type_validation.py b/api/tests/unit_tests/core/variables/test_segment_type_validation.py index 317fe99d37..1752e27271 100644 --- a/api/tests/unit_tests/core/variables/test_segment_type_validation.py +++ b/api/tests/unit_tests/core/variables/test_segment_type_validation.py @@ -9,7 +9,6 @@ from dataclasses import dataclass from typing import Any import pytest - from graphon.file import File, FileTransferMethod, FileType from graphon.variables.segment_group import SegmentGroup from graphon.variables.segments import ( diff --git a/api/tests/unit_tests/core/variables/test_variables.py b/api/tests/unit_tests/core/variables/test_variables.py index dae5e1ce98..75b01bf42e 100644 --- a/api/tests/unit_tests/core/variables/test_variables.py +++ b/api/tests/unit_tests/core/variables/test_variables.py @@ -1,6 +1,4 @@ import pytest -from pydantic import ValidationError - from graphon.variables import ( ArrayFileVariable, ArrayVariable, @@ -12,6 +10,7 @@ from graphon.variables import ( StringVariable, ) from graphon.variables.variables import VariableBase +from pydantic import ValidationError def test_frozen_variables(): diff --git a/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py b/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py index 025d79b25d..41627f5e0b 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py @@ -5,13 +5,12 @@ Shared fixtures for ObservabilityLayer tests. from unittest.mock import MagicMock, patch import pytest +from graphon.enums import BuiltinNodeTypes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace import set_tracer_provider -from graphon.enums import BuiltinNodeTypes - @pytest.fixture def memory_span_exporter(): @@ -62,9 +61,10 @@ def mock_llm_node(): @pytest.fixture def mock_tool_node(): """Create a mock Tool Node with tool-specific attributes.""" - from core.tools.entities.tool_entities import ToolProviderType from graphon.nodes.tool.entities import ToolNodeData + from core.tools.entities.tool_entities import ToolProviderType + node = MagicMock() node.id = "test-tool-node-id" node.title = "Test Tool Node" diff --git a/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py b/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py index 919f15efd0..9cf72763ee 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py @@ -13,10 +13,10 @@ Test coverage: from unittest.mock import patch import pytest +from graphon.enums import BuiltinNodeTypes from opentelemetry.trace import StatusCode from core.app.workflow.layers.observability import ObservabilityLayer -from graphon.enums import BuiltinNodeTypes class TestObservabilityLayerInitialization: diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py index f9819c47ec..27938e084b 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py @@ -10,10 +10,6 @@ from collections.abc import Generator, Mapping from typing import TYPE_CHECKING, Any, Optional from unittest.mock import MagicMock -from core.model_manager import ModelInstance -from core.workflow.node_runtime import DifyToolNodeRuntime -from core.workflow.nodes.agent import AgentNode -from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent @@ -31,6 +27,11 @@ from graphon.nodes.template_transform import TemplateTransformNode from graphon.nodes.tool import ToolNode from graphon.template_rendering import Jinja2TemplateRenderer, TemplateRenderError +from core.model_manager import ModelInstance +from core.workflow.node_runtime import DifyToolNodeRuntime +from core.workflow.nodes.agent import AgentNode +from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode + if TYPE_CHECKING: from graphon.entities import GraphInitParams from graphon.runtime import GraphRuntimeState diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py b/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py index 75bc6d05f7..d3272936cd 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py @@ -4,13 +4,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Protocol -from core.repositories.human_input_repository import ( - FormCreateParams, - HumanInputFormEntity, - HumanInputFormRepository, -) -from core.workflow.node_runtime import DifyHumanInputNodeRuntime -from core.workflow.system_variables import build_system_variables from graphon.entities import WorkflowStartReason from graphon.graph import Graph from graphon.graph_engine import GraphEngine, GraphEngineConfig @@ -30,6 +23,14 @@ from graphon.nodes.human_input.human_input_node import HumanInputNode from graphon.nodes.start.entities import StartNodeData from graphon.nodes.start.start_node import StartNode from graphon.runtime import GraphRuntimeState, VariablePool + +from core.repositories.human_input_repository import ( + FormCreateParams, + HumanInputFormEntity, + HumanInputFormRepository, +) +from core.workflow.node_runtime import DifyHumanInputNodeRuntime +from core.workflow.system_variables import build_system_variables from libs.datetime_utils import naive_utc_now from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py b/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py index 7d23b63049..b11f957677 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py @@ -19,11 +19,6 @@ from functools import lru_cache from pathlib import Path from typing import Any -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom -from core.tools.utils.yaml_utils import _load_yaml_file -from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id -from core.workflow.system_variables import build_bootstrap_variables, build_system_variables -from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool from graphon.entities import GraphInitParams from graphon.graph import Graph from graphon.graph_engine import GraphEngine, GraphEngineConfig @@ -44,6 +39,12 @@ from graphon.variables import ( StringVariable, ) +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom +from core.tools.utils.yaml_utils import _load_yaml_file +from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id +from core.workflow.system_variables import build_bootstrap_variables, build_system_variables +from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool + from .test_mock_config import MockConfig from .test_mock_factory import MockNodeFactory diff --git a/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py b/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py index ec4cef1955..343bcd3919 100644 --- a/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py @@ -1,10 +1,10 @@ import pytest - -from core.workflow.node_factory import get_node_type_classes_mapping from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.base.node import Node +from core.workflow.node_factory import get_node_type_classes_mapping + # Ensures that all production node classes are imported and registered. _ = get_node_type_classes_mapping() diff --git a/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py b/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py index ef0df55995..b9371a34f4 100644 --- a/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py +++ b/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py @@ -1,7 +1,6 @@ import types from collections.abc import Mapping -from core.workflow.node_factory import get_node_type_classes_mapping from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.base.node import Node @@ -14,6 +13,8 @@ from graphon.nodes.variable_assigner.v2.node import ( VariableAssignerNode as VariableAssignerV2, ) +from core.workflow.node_factory import get_node_type_classes_mapping + def test_variable_assigner_latest_prefers_highest_numeric_version(): # Act diff --git a/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py index ce0c9b79c6..d155124c50 100644 --- a/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py @@ -1,4 +1,3 @@ -from configs import dify_config from graphon.nodes.code.code_node import CodeNode from graphon.nodes.code.entities import CodeLanguage, CodeNodeData from graphon.nodes.code.exc import ( @@ -9,6 +8,8 @@ from graphon.nodes.code.exc import ( from graphon.nodes.code.limits import CodeNodeLimits from graphon.variables.types import SegmentType +from configs import dify_config + CodeNode._limits = CodeNodeLimits( max_string_length=dify_config.CODE_MAX_STRING_LENGTH, max_number=dify_config.CODE_MAX_NUMBER, diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py index be7cc073db..a5026b40cf 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -1,8 +1,4 @@ import pytest - -from configs import dify_config -from core.helper.ssrf_proxy import ssrf_proxy -from core.workflow.system_variables import default_system_variables from graphon.file.file_manager import file_manager from graphon.nodes.http_request import ( BodyData, @@ -16,6 +12,10 @@ from graphon.nodes.http_request.exc import AuthorizationConfigError from graphon.nodes.http_request.executor import Executor from graphon.runtime import VariablePool +from configs import dify_config +from core.helper.ssrf_proxy import ssrf_proxy +from core.workflow.system_variables import default_system_variables + HTTP_REQUEST_CONFIG = HttpRequestNodeConfig( max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT, max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT, diff --git a/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py b/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py index 4a9438b14f..bbb4fa5f69 100644 --- a/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py +++ b/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py @@ -1,9 +1,6 @@ import datetime from types import SimpleNamespace -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom -from core.workflow.node_runtime import DifyHumanInputNodeRuntime -from core.workflow.system_variables import default_system_variables from graphon.entities import GraphInitParams from graphon.enums import BuiltinNodeTypes from graphon.graph_events import ( @@ -15,6 +12,10 @@ from graphon.nodes.human_input.entities import HumanInputNodeData from graphon.nodes.human_input.enums import HumanInputFormStatus from graphon.nodes.human_input.human_input_node import HumanInputNode from graphon.runtime import GraphRuntimeState, VariablePool + +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom +from core.workflow.node_runtime import DifyHumanInputNodeRuntime +from core.workflow.system_variables import default_system_variables from libs.datetime_utils import naive_utc_now diff --git a/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py b/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py index e923ee761b..3ab86001e8 100644 --- a/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py @@ -3,6 +3,10 @@ import uuid from unittest.mock import Mock import pytest +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variables import StringSegment from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.workflow.nodes.knowledge_retrieval.entities import ( @@ -21,10 +25,6 @@ from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import ( ) from core.workflow.nodes.knowledge_retrieval.retrieval import RAGRetrievalProtocol, Source from core.workflow.system_variables import build_system_variables -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variables import StringSegment from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py b/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py index 388654f279..c0332fe99a 100644 --- a/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py @@ -2,8 +2,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest - -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY from graphon.entities import GraphInitParams from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.nodes.list_operator.entities import ListOperatorNodeData @@ -11,6 +9,8 @@ from graphon.nodes.list_operator.node import ListOperatorNode from graphon.runtime import GraphRuntimeState from graphon.variables import ArrayNumberSegment, ArrayStringSegment +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY + class TestListOperatorNode: """Comprehensive tests for ListOperatorNode.""" diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index c707cf28cd..453c9dba4d 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -5,19 +5,6 @@ from collections.abc import Sequence from unittest import mock import pytest - -from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, ModelConfigWithCredentialsEntity, UserFrom -from core.app.llm.model_access import ( - DifyCredentialsProvider, - DifyModelFactory, - build_dify_model_access, - fetch_model_config, -) -from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle -from core.entities.provider_entities import CustomConfiguration, SystemConfiguration -from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime -from core.prompt.entities.advanced_prompt_entities import MemoryConfig -from core.workflow.system_variables import default_system_variables from graphon.entities import GraphInitParams from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities.common_entities import I18nObject @@ -80,6 +67,19 @@ from graphon.nodes.llm.runtime_protocols import PromptMessageSerializerProtocol from graphon.runtime import GraphRuntimeState, VariablePool from graphon.template_rendering import TemplateRenderError from graphon.variables import ArrayAnySegment, ArrayFileSegment, NoneSegment + +from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, ModelConfigWithCredentialsEntity, UserFrom +from core.app.llm.model_access import ( + DifyCredentialsProvider, + DifyModelFactory, + build_dify_model_access, + fetch_model_config, +) +from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle +from core.entities.provider_entities import CustomConfiguration, SystemConfiguration +from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime +from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from core.workflow.system_variables import default_system_variables from models.provider import ProviderType from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py index 8f8ec49f14..1c362a0a03 100644 --- a/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py @@ -6,8 +6,6 @@ from dataclasses import dataclass from typing import Any import pytest - -from factories.variable_factory import build_segment_with_type from graphon.model_runtime.entities import LLMMode from graphon.nodes.llm import ModelConfig, VisionConfig from graphon.nodes.parameter_extractor.entities import ParameterConfig, ParameterExtractorNodeData @@ -20,6 +18,8 @@ from graphon.nodes.parameter_extractor.exc import ( from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode from graphon.variables.types import SegmentType +from factories.variable_factory import build_segment_with_type + @dataclass class ValidTestCase: diff --git a/api/tests/unit_tests/core/workflow/nodes/test_base_node.py b/api/tests/unit_tests/core/workflow/nodes/test_base_node.py index 364408ead6..824f52a89b 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_base_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_base_node.py @@ -1,15 +1,15 @@ from collections.abc import Mapping import pytest - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.node_runtime import resolve_dify_run_context -from core.workflow.system_variables import build_system_variables from graphon.entities import GraphInitParams from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes from graphon.nodes.base.node import Node from graphon.runtime import GraphRuntimeState, VariablePool + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.node_runtime import resolve_dify_run_context +from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py index dd75b32593..896ec61092 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py @@ -4,8 +4,6 @@ from unittest.mock import Mock, patch import pandas as pd import pytest from docx.oxml.text.paragraph import CT_P - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from graphon.entities import GraphInitParams from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod @@ -23,6 +21,8 @@ from graphon.nodes.document_extractor.node import ( from graphon.variables import ArrayFileSegment, FileSegment from graphon.variables.segments import ArrayStringSegment from graphon.variables.variables import StringVariable + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/test_if_else.py b/api/tests/unit_tests/core/workflow/nodes/test_if_else.py index aa9a1360b0..18cb136322 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_if_else.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_if_else.py @@ -3,11 +3,6 @@ import uuid from unittest.mock import MagicMock, Mock import pytest - -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables -from extensions.ext_database import db from graphon.enums import WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod, FileType from graphon.graph import Graph @@ -16,6 +11,11 @@ from graphon.nodes.if_else.if_else_node import IfElseNode from graphon.runtime import GraphRuntimeState, VariablePool from graphon.utils.condition.entities import Condition, SubCondition, SubVariableCondition from graphon.variables import ArrayFileSegment + +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables +from extensions.ext_database import db from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py index 465a4c0ff4..e7bf78f851 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py @@ -1,8 +1,6 @@ from unittest.mock import MagicMock import pytest - -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom from graphon.enums import WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod, FileType from graphon.nodes.list_operator.entities import ( @@ -18,6 +16,8 @@ from graphon.nodes.list_operator.exc import InvalidKeyError from graphon.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func from graphon.variables import ArrayFileSegment +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom + def _build_list_operator_node(node_data: ListOperatorNodeData, graph_init_params) -> ListOperatorNode: return ListOperatorNode( diff --git a/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py b/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py index 5655f80737..56bbb923b3 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py @@ -2,16 +2,16 @@ import json import time import pytest -from pydantic import ValidationError as PydanticValidationError - -from core.workflow.system_variables import build_system_variables -from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID from graphon.nodes.start.entities import StartNodeData from graphon.nodes.start.start_node import StartNode from graphon.runtime import GraphRuntimeState from graphon.variables import build_segment, segment_to_variable from graphon.variables.input_entities import VariableEntity, VariableEntityType from graphon.variables.variables import Variable +from pydantic import ValidationError as PydanticValidationError + +from core.workflow.system_variables import build_system_variables +from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID from tests.workflow_test_utils import build_test_graph_init_params, build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py b/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py index 617554ee17..f1132af02b 100644 --- a/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py +++ b/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py @@ -1,4 +1,5 @@ import pytest +from graphon.entities.exc import BaseNodeError from core.workflow.nodes.trigger_webhook.exc import ( WebhookConfigError, @@ -6,7 +7,6 @@ from core.workflow.nodes.trigger_webhook.exc import ( WebhookNotFoundError, WebhookTimeoutError, ) -from graphon.entities.exc import BaseNodeError def test_webhook_node_error_inheritance(): diff --git a/api/tests/unit_tests/core/workflow/test_variable_pool.py b/api/tests/unit_tests/core/workflow/test_variable_pool.py index 9dab38ed8e..21998af02f 100644 --- a/api/tests/unit_tests/core/workflow/test_variable_pool.py +++ b/api/tests/unit_tests/core/workflow/test_variable_pool.py @@ -3,15 +3,6 @@ import uuid from collections import defaultdict import pytest - -from core.workflow.system_variables import build_system_variables, system_variables_to_mapping -from core.workflow.variable_pool_initializer import add_variables_to_pool -from core.workflow.variable_prefixes import ( - CONVERSATION_VARIABLE_NODE_ID, - ENVIRONMENT_VARIABLE_NODE_ID, - SYSTEM_VARIABLE_NODE_ID, -) -from factories.variable_factory import build_segment, segment_to_variable from graphon.file import File, FileTransferMethod, FileType from graphon.runtime import VariablePool from graphon.variables import FileSegment, StringSegment @@ -38,6 +29,15 @@ from graphon.variables.variables import ( ) from models.utils.file_input_compat import rebuild_serialized_graph_files_without_lookup +from core.workflow.system_variables import build_system_variables, system_variables_to_mapping +from core.workflow.variable_pool_initializer import add_variables_to_pool +from core.workflow.variable_prefixes import ( + CONVERSATION_VARIABLE_NODE_ID, + ENVIRONMENT_VARIABLE_NODE_ID, + SYSTEM_VARIABLE_NODE_ID, +) +from factories.variable_factory import build_segment, segment_to_variable + @pytest.fixture def pool(): diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry.py b/api/tests/unit_tests/core/workflow/test_workflow_entry.py index 041c5cc612..d8361d06c4 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry.py @@ -1,6 +1,12 @@ from types import SimpleNamespace import pytest +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.file import File, FileTransferMethod, FileType +from graphon.nodes.code.code_node import CodeNode +from graphon.nodes.code.limits import CodeNodeLimits +from graphon.runtime import VariablePool +from graphon.variables.variables import StringVariable from configs import dify_config from core.helper.code_executor.code_executor import CodeLanguage @@ -10,12 +16,6 @@ from core.workflow.variable_prefixes import ( ENVIRONMENT_VARIABLE_NODE_ID, ) from core.workflow.workflow_entry import WorkflowEntry -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.file import File, FileTransferMethod, FileType -from graphon.nodes.code.code_node import CodeNode -from graphon.nodes.code.limits import CodeNodeLimits -from graphon.runtime import VariablePool -from graphon.variables.variables import StringVariable @pytest.fixture(autouse=True) diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py b/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py index 80dc8927fa..4b2f98aeff 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py @@ -2,11 +2,12 @@ from unittest.mock import MagicMock, patch -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.workflow_entry import WorkflowEntry from graphon.graph_engine.command_channels import RedisChannel from graphon.runtime import GraphRuntimeState, VariablePool +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.workflow_entry import WorkflowEntry + class TestWorkflowEntryRedisChannel: """Test suite for WorkflowEntry with Redis command channel.""" diff --git a/api/tests/unit_tests/factories/test_build_from_mapping.py b/api/tests/unit_tests/factories/test_build_from_mapping.py index ffb151fbf4..378aa9744c 100644 --- a/api/tests/unit_tests/factories/test_build_from_mapping.py +++ b/api/tests/unit_tests/factories/test_build_from_mapping.py @@ -2,13 +2,13 @@ import uuid from unittest.mock import MagicMock, patch import pytest +from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig from httpx import Response from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.app.file_access import DatabaseFileAccessController, FileAccessScope, bind_file_access_scope from core.workflow.file_reference import build_file_reference, parse_file_reference, resolve_file_record_id from factories.file_factory.builders import build_from_mapping as _build_from_mapping -from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig from models import ToolFile, UploadFile diff --git a/api/tests/unit_tests/factories/test_variable_factory.py b/api/tests/unit_tests/factories/test_variable_factory.py index 2439409e80..015d1f8929 100644 --- a/api/tests/unit_tests/factories/test_variable_factory.py +++ b/api/tests/unit_tests/factories/test_variable_factory.py @@ -4,11 +4,6 @@ from typing import Any from uuid import uuid4 import pytest -from hypothesis import HealthCheck, given, settings -from hypothesis import strategies as st - -from factories import variable_factory -from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type from graphon.file import File, FileTransferMethod, FileType from graphon.variables import ( ArrayNumberVariable, @@ -36,6 +31,11 @@ from graphon.variables.segments import ( StringSegment, ) from graphon.variables.types import SegmentType +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from factories import variable_factory +from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type def test_string_variable(): diff --git a/api/tests/unit_tests/libs/_human_input/test_form_service.py b/api/tests/unit_tests/libs/_human_input/test_form_service.py index fa2c02020b..f1ce1a2c1c 100644 --- a/api/tests/unit_tests/libs/_human_input/test_form_service.py +++ b/api/tests/unit_tests/libs/_human_input/test_form_service.py @@ -5,7 +5,6 @@ Unit tests for FormService. from datetime import timedelta import pytest - from graphon.nodes.human_input.entities import ( FormInput, UserAction, @@ -14,6 +13,7 @@ from graphon.nodes.human_input.enums import ( FormInputType, TimeoutUnit, ) + from libs.datetime_utils import naive_utc_now from .support import ( diff --git a/api/tests/unit_tests/libs/_human_input/test_models.py b/api/tests/unit_tests/libs/_human_input/test_models.py index 866ee61b3e..0babfbb315 100644 --- a/api/tests/unit_tests/libs/_human_input/test_models.py +++ b/api/tests/unit_tests/libs/_human_input/test_models.py @@ -5,7 +5,6 @@ Unit tests for human input form models. from datetime import datetime, timedelta import pytest - from graphon.nodes.human_input.entities import ( FormInput, UserAction, @@ -14,6 +13,7 @@ from graphon.nodes.human_input.enums import ( FormInputType, TimeoutUnit, ) + from libs.datetime_utils import naive_utc_now from .support import FormSubmissionData, FormSubmissionRequest, HumanInputForm diff --git a/api/tests/unit_tests/models/test_conversation_variable.py b/api/tests/unit_tests/models/test_conversation_variable.py index bb3a6db1a1..86163f1554 100644 --- a/api/tests/unit_tests/models/test_conversation_variable.py +++ b/api/tests/unit_tests/models/test_conversation_variable.py @@ -1,7 +1,8 @@ from uuid import uuid4 -from factories import variable_factory from graphon.variables import SegmentType + +from factories import variable_factory from models import ConversationVariable diff --git a/api/tests/unit_tests/models/test_model.py b/api/tests/unit_tests/models/test_model.py index a87dd7f15a..3f6d6bfbe3 100644 --- a/api/tests/unit_tests/models/test_model.py +++ b/api/tests/unit_tests/models/test_model.py @@ -2,9 +2,9 @@ import importlib import types import pytest +from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod from core.workflow.file_reference import build_file_reference -from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod from models.model import Conversation, Message diff --git a/api/tests/unit_tests/models/test_workflow.py b/api/tests/unit_tests/models/test_workflow.py index d80c0f45ff..d2949ef259 100644 --- a/api/tests/unit_tests/models/test_workflow.py +++ b/api/tests/unit_tests/models/test_workflow.py @@ -3,13 +3,14 @@ import json from unittest import mock from uuid import uuid4 +from graphon.file import File, FileTransferMethod, FileType +from graphon.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable +from graphon.variables.segments import IntegerSegment, Segment + from constants import HIDDEN_VALUE from core.helper import encrypter from core.workflow.file_reference import build_file_reference from factories.variable_factory import build_segment -from graphon.file import File, FileTransferMethod, FileType -from graphon.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable -from graphon.variables.segments import IntegerSegment, Segment from models.workflow import ( Workflow, WorkflowDraftVariable, diff --git a/api/tests/unit_tests/models/test_workflow_models.py b/api/tests/unit_tests/models/test_workflow_models.py index eb9fef7587..507e1c8c3a 100644 --- a/api/tests/unit_tests/models/test_workflow_models.py +++ b/api/tests/unit_tests/models/test_workflow_models.py @@ -13,12 +13,12 @@ from datetime import UTC, datetime from uuid import uuid4 import pytest - from graphon.enums import ( BuiltinNodeTypes, WorkflowExecutionStatus, WorkflowNodeExecutionStatus, ) + from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import ( Workflow, diff --git a/api/tests/unit_tests/services/document_service_validation.py b/api/tests/unit_tests/services/document_service_validation.py index 71df8c4e20..6903c47a24 100644 --- a/api/tests/unit_tests/services/document_service_validation.py +++ b/api/tests/unit_tests/services/document_service_validation.py @@ -109,11 +109,11 @@ This test suite follows a comprehensive testing strategy that covers: from unittest.mock import Mock, patch import pytest +from graphon.model_runtime.entities.model_entities import ModelType from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError from core.rag.entities import PreProcessingRule, Rule, Segmentation from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType -from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, DatasetProcessRule, Document from services.dataset_service import DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import ( diff --git a/api/tests/unit_tests/services/test_conversation_service.py b/api/tests/unit_tests/services/test_conversation_service.py index 2c7f13b79f..68f4c51afe 100644 --- a/api/tests/unit_tests/services/test_conversation_service.py +++ b/api/tests/unit_tests/services/test_conversation_service.py @@ -6,15 +6,26 @@ Tests are organized by functionality and include edge cases, error handling, and both positive and negative test scenarios. """ +from datetime import timedelta from unittest.mock import MagicMock, Mock, create_autospec, patch +import pytest from sqlalchemy import asc, desc from core.app.entities.app_invoke_entities import InvokeFrom from libs.datetime_utils import naive_utc_now +from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account, ConversationVariable +from models.enums import ConversationFromSource from models.model import App, Conversation, EndUser, Message from services.conversation_service import ConversationService +from services.errors.conversation import ( + ConversationNotExistsError, + ConversationVariableNotExistsError, + ConversationVariableTypeMismatchError, + LastConversationNotExistsError, +) +from services.errors.message import MessageNotExistsError class ConversationServiceTestDataFactory: @@ -327,9 +338,330 @@ class TestConversationServiceHelpers: assert condition is not None +class TestConversationServiceGetConversation: + """Test conversation retrieval operations.""" + + @patch("services.conversation_service.db.session") + def test_get_conversation_success_with_account(self, mock_db_session): + """ + Test successful conversation retrieval with account user. + + Should return conversation when found with proper filters. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock( + from_account_id=user.id, from_source=ConversationFromSource.CONSOLE + ) + + mock_db_session.scalar.return_value = conversation + + # Act + result = ConversationService.get_conversation(app_model, "conv-123", user) + + # Assert + assert result == conversation + + @patch("services.conversation_service.db.session") + def test_get_conversation_success_with_end_user(self, mock_db_session): + """ + Test successful conversation retrieval with end user. + + Should return conversation when found with proper filters for API user. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_end_user_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock( + from_end_user_id=user.id, from_source=ConversationFromSource.API + ) + + mock_db_session.scalar.return_value = conversation + + # Act + result = ConversationService.get_conversation(app_model, "conv-123", user) + + # Assert + assert result == conversation + + @patch("services.conversation_service.db.session") + def test_get_conversation_not_found_raises_error(self, mock_db_session): + """ + Test that get_conversation raises error when conversation not found. + + Should raise ConversationNotExistsError when no matching conversation found. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + mock_db_session.scalar.return_value = None + + # Act & Assert + with pytest.raises(ConversationNotExistsError): + ConversationService.get_conversation(app_model, "conv-123", user) + + +class TestConversationServiceRename: + """Test conversation rename operations.""" + + @patch("services.conversation_service.db.session") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_rename_with_manual_name(self, mock_get_conversation, mock_db_session): + """ + Test renaming conversation with manual name. + + Should update conversation name and timestamp when auto_generate is False. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Act + result = ConversationService.rename( + app_model=app_model, + conversation_id="conv-123", + user=user, + name="New Name", + auto_generate=False, + ) + + # Assert + assert result == conversation + assert conversation.name == "New Name" + mock_db_session.commit.assert_called_once() + + +class TestConversationServiceAutoGenerateName: + """Test conversation auto-name generation operations.""" + + @patch("services.conversation_service.db.session") + @patch("services.conversation_service.LLMGenerator") + def test_auto_generate_name_success(self, mock_llm_generator, mock_db_session): + """ + Test successful auto-generation of conversation name. + + Should generate name using LLMGenerator and update conversation. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + message = ConversationServiceTestDataFactory.create_message_mock( + conversation_id=conversation.id, app_id=app_model.id + ) + + # Mock database query to return message + mock_db_session.scalar.return_value = message + + # Mock LLM generator + mock_llm_generator.generate_conversation_name.return_value = "Generated Name" + + # Act + result = ConversationService.auto_generate_name(app_model, conversation) + + # Assert + assert result == conversation + assert conversation.name == "Generated Name" + mock_llm_generator.generate_conversation_name.assert_called_once_with( + app_model.tenant_id, message.query, conversation.id, app_model.id + ) + mock_db_session.commit.assert_called_once() + + @patch("services.conversation_service.db.session") + def test_auto_generate_name_no_message_raises_error(self, mock_db_session): + """ + Test auto-generation fails when no message found. + + Should raise MessageNotExistsError when conversation has no messages. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + # Mock database query to return None + mock_db_session.scalar.return_value = None + + # Act & Assert + with pytest.raises(MessageNotExistsError): + ConversationService.auto_generate_name(app_model, conversation) + + @patch("services.conversation_service.db.session") + @patch("services.conversation_service.LLMGenerator") + def test_auto_generate_name_handles_llm_exception(self, mock_llm_generator, mock_db_session): + """ + Test auto-generation handles LLM generator exceptions gracefully. + + Should continue without name when LLMGenerator fails. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + message = ConversationServiceTestDataFactory.create_message_mock( + conversation_id=conversation.id, app_id=app_model.id + ) + + # Mock database query to return message + mock_db_session.scalar.return_value = message + + # Mock LLM generator to raise exception + mock_llm_generator.generate_conversation_name.side_effect = Exception("LLM Error") + + # Act + result = ConversationService.auto_generate_name(app_model, conversation) + + # Assert + assert result == conversation + # Name should remain unchanged due to exception + mock_db_session.commit.assert_called_once() + + +class TestConversationServiceDelete: + """Test conversation deletion operations.""" + + @patch("services.conversation_service.delete_conversation_related_data") + @patch("services.conversation_service.db.session") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_delete_success(self, mock_get_conversation, mock_db_session, mock_delete_task): + """ + Test successful conversation deletion. + + Should delete conversation and schedule cleanup task. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock(name="Test App") + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Act + ConversationService.delete(app_model, "conv-123", user) + + # Assert + mock_db_session.delete.assert_called_once_with(conversation) + mock_db_session.commit.assert_called_once() + mock_delete_task.delay.assert_called_once_with(conversation.id) + + class TestConversationServiceConversationalVariable: """Test conversational variable operations.""" + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_get_conversational_variable_success(self, mock_get_conversation, mock_session_factory): + """ + Test successful retrieval of conversational variables. + + Should return paginated list of variables for conversation. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session and variables + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + variable1 = ConversationServiceTestDataFactory.create_conversation_variable_mock() + variable2 = ConversationServiceTestDataFactory.create_conversation_variable_mock(variable_id="var-456") + + mock_session.scalars.return_value.all.return_value = [variable1, variable2] + + # Act + result = ConversationService.get_conversational_variable( + app_model=app_model, + conversation_id="conv-123", + user=user, + limit=10, + last_id=None, + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + assert len(result.data) == 2 + assert result.limit == 10 + assert result.has_more is False + + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_get_conversational_variable_with_last_id(self, mock_get_conversation, mock_session_factory): + """ + Test retrieval of variables with last_id pagination. + + Should filter variables created after last_id. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session and variables + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + last_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock( + created_at=naive_utc_now() - timedelta(hours=1) + ) + variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=naive_utc_now()) + + mock_session.scalar.return_value = last_variable + mock_session.scalars.return_value.all.return_value = [variable] + + # Act + result = ConversationService.get_conversational_variable( + app_model=app_model, + conversation_id="conv-123", + user=user, + limit=10, + last_id="var-123", + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + assert len(result.data) == 1 + assert result.limit == 10 + + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_get_conversational_variable_last_id_not_found_raises_error( + self, mock_get_conversation, mock_session_factory + ): + """ + Test that invalid last_id raises ConversationVariableNotExistsError. + + Should raise error when last_id doesn't exist. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = None + + # Act & Assert + with pytest.raises(ConversationVariableNotExistsError): + ConversationService.get_conversational_variable( + app_model=app_model, + conversation_id="conv-123", + user=user, + limit=10, + last_id="invalid-id", + ) + @patch("services.conversation_service.session_factory") @patch("services.conversation_service.ConversationService.get_conversation") @patch("services.conversation_service.dify_config") @@ -366,3 +698,466 @@ class TestConversationServiceConversationalVariable: # Assert - JSON filter should be applied assert mock_session.scalars.called + + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + @patch("services.conversation_service.dify_config") + def test_get_conversational_variable_with_name_filter_postgresql( + self, mock_config, mock_get_conversation, mock_session_factory + ): + """ + Test variable filtering by name for PostgreSQL databases. + + Should apply JSON extraction filter for variable names. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + mock_config.DB_TYPE = "postgresql" + + # Mock session + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + mock_session.scalars.return_value.all.return_value = [] + + # Act + ConversationService.get_conversational_variable( + app_model=app_model, + conversation_id="conv-123", + user=user, + limit=10, + last_id=None, + variable_name="test_var", + ) + + # Assert - JSON filter should be applied + assert mock_session.scalars.called + + +class TestConversationServiceUpdateVariable: + """Test conversation variable update operations.""" + + @patch("services.conversation_service.variable_factory") + @patch("services.conversation_service.ConversationVariableUpdater") + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_update_conversation_variable_success( + self, mock_get_conversation, mock_session_factory, mock_updater_class, mock_variable_factory + ): + """ + Test successful update of conversation variable. + + Should update variable value and return updated data. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session and existing variable + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="string") + mock_session.scalar.return_value = existing_variable + + # Mock variable factory and updater + updated_variable = Mock() + updated_variable.model_dump.return_value = {"id": "var-123", "name": "test_var", "value": "new_value"} + mock_variable_factory.build_conversation_variable_from_mapping.return_value = updated_variable + + mock_updater = MagicMock() + mock_updater_class.return_value = mock_updater + + # Act + result = ConversationService.update_conversation_variable( + app_model=app_model, + conversation_id="conv-123", + variable_id="var-123", + user=user, + new_value="new_value", + ) + + # Assert + assert result["id"] == "var-123" + assert result["value"] == "new_value" + mock_updater.update.assert_called_once_with("conv-123", updated_variable) + mock_updater.flush.assert_called_once() + + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_update_conversation_variable_not_found_raises_error(self, mock_get_conversation, mock_session_factory): + """ + Test update fails when variable doesn't exist. + + Should raise ConversationVariableNotExistsError. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = None + + # Act & Assert + with pytest.raises(ConversationVariableNotExistsError): + ConversationService.update_conversation_variable( + app_model=app_model, + conversation_id="conv-123", + variable_id="invalid-id", + user=user, + new_value="new_value", + ) + + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_update_conversation_variable_type_mismatch_raises_error(self, mock_get_conversation, mock_session_factory): + """ + Test update fails when value type doesn't match expected type. + + Should raise ConversationVariableTypeMismatchError. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session and existing variable + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="number") + mock_session.scalar.return_value = existing_variable + + # Act & Assert - Try to set string value for number variable + with pytest.raises(ConversationVariableTypeMismatchError): + ConversationService.update_conversation_variable( + app_model=app_model, + conversation_id="conv-123", + variable_id="var-123", + user=user, + new_value="string_value", # Wrong type + ) + + @patch("services.conversation_service.session_factory") + @patch("services.conversation_service.ConversationService.get_conversation") + def test_update_conversation_variable_integer_number_compatibility( + self, mock_get_conversation, mock_session_factory + ): + """ + Test that integer type accepts number values. + + Should allow number values for integer type variables. + """ + # Arrange + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + + mock_get_conversation.return_value = conversation + + # Mock session and existing variable + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="integer") + mock_session.scalar.return_value = existing_variable + + # Mock variable factory and updater + updated_variable = Mock() + updated_variable.model_dump.return_value = {"id": "var-123", "name": "test_var", "value": 42} + + with ( + patch("services.conversation_service.variable_factory") as mock_variable_factory, + patch("services.conversation_service.ConversationVariableUpdater") as mock_updater_class, + ): + mock_variable_factory.build_conversation_variable_from_mapping.return_value = updated_variable + mock_updater = MagicMock() + mock_updater_class.return_value = mock_updater + + # Act + result = ConversationService.update_conversation_variable( + app_model=app_model, + conversation_id="conv-123", + variable_id="var-123", + user=user, + new_value=42, # Number value for integer type + ) + + # Assert + assert result["value"] == 42 + mock_updater.update.assert_called_once() + + +class TestConversationServicePaginationAdvanced: + """Advanced pagination tests for ConversationService.""" + + @patch("services.conversation_service.session_factory") + def test_pagination_by_last_id_with_last_id_not_found(self, mock_session_factory): + """ + Test pagination with invalid last_id raises error. + + Should raise LastConversationNotExistsError when last_id doesn't exist. + """ + # Arrange + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = None + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Act & Assert + with pytest.raises(LastConversationNotExistsError): + ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id="invalid-id", + limit=20, + invoke_from=InvokeFrom.WEB_APP, + ) + + @patch("services.conversation_service.session_factory") + def test_pagination_by_last_id_with_exclude_ids(self, mock_session_factory): + """ + Test pagination with exclude_ids filter. + + Should exclude specified conversation IDs from results. + """ + # Arrange + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + mock_session.scalars.return_value.all.return_value = [conversation] + mock_session.scalar.return_value = conversation + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + exclude_ids=["excluded-123"], + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + assert len(result.data) == 1 + + @patch("services.conversation_service.session_factory") + def test_pagination_by_last_id_has_more_detection(self, mock_session_factory): + """ + Test pagination has_more detection logic. + + Should set has_more=True when there are more results beyond limit. + """ + # Arrange + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + # Return exactly limit items to trigger has_more check + conversations = [ + ConversationServiceTestDataFactory.create_conversation_mock(conversation_id=f"conv-{i}") for i in range(20) + ] + mock_session.scalars.return_value.all.return_value = conversations + mock_session.scalar.return_value = conversations[-1] + + # Mock count query to return > 0 + mock_session.scalar.return_value = 5 # Additional items exist + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + assert result.has_more is True + + @patch("services.conversation_service.session_factory") + def test_pagination_by_last_id_with_different_sort_by(self, mock_session_factory): + """ + Test pagination with different sort fields. + + Should handle various sort_by parameters correctly. + """ + # Arrange + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + conversation = ConversationServiceTestDataFactory.create_conversation_mock() + mock_session.scalars.return_value.all.return_value = [conversation] + mock_session.scalar.return_value = conversation + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Test different sort fields + sort_fields = ["created_at", "-updated_at", "name", "-status"] + + for sort_by in sort_fields: + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + sort_by=sort_by, + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + + +class TestConversationServiceEdgeCases: + """Test edge cases and error scenarios.""" + + @patch("services.conversation_service.session_factory") + def test_pagination_with_end_user_api_source(self, mock_session_factory): + """ + Test pagination correctly handles EndUser with API source. + + Should use 'api' as from_source for EndUser instances. + """ + # Arrange + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + conversation = ConversationServiceTestDataFactory.create_conversation_mock( + from_source=ConversationFromSource.API, from_end_user_id="user-123" + ) + mock_session.scalars.return_value.all.return_value = [conversation] + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_end_user_mock() + + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + + @patch("services.conversation_service.session_factory") + def test_pagination_with_account_console_source(self, mock_session_factory): + """ + Test pagination correctly handles Account with console source. + + Should use 'console' as from_source for Account instances. + """ + # Arrange + mock_session = MagicMock() + mock_session_factory.create_session.return_value.__enter__.return_value = mock_session + + conversation = ConversationServiceTestDataFactory.create_conversation_mock( + from_source=ConversationFromSource.CONSOLE, from_account_id="account-123" + ) + mock_session.scalars.return_value.all.return_value = [conversation] + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + + def test_pagination_with_include_ids_filter(self): + """ + Test pagination with include_ids filter. + + Should only return conversations with IDs in include_ids list. + """ + # Arrange + mock_session = MagicMock() + mock_session.scalars.return_value.all.return_value = [] + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + include_ids=["conv-123", "conv-456"], + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + # Verify that include_ids filter was applied + assert mock_session.scalars.called + + def test_pagination_with_empty_exclude_ids(self): + """ + Test pagination with empty exclude_ids list. + + Should handle empty exclude_ids gracefully. + """ + # Arrange + mock_session = MagicMock() + mock_session.scalars.return_value.all.return_value = [] + + app_model = ConversationServiceTestDataFactory.create_app_mock() + user = ConversationServiceTestDataFactory.create_account_mock() + + # Act + result = ConversationService.pagination_by_last_id( + session=mock_session, + app_model=app_model, + user=user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + exclude_ids=[], + ) + + # Assert + assert isinstance(result, InfiniteScrollPagination) + assert result.has_more is False diff --git a/api/tests/unit_tests/services/test_human_input_service.py b/api/tests/unit_tests/services/test_human_input_service.py index 55af564821..9be475d043 100644 --- a/api/tests/unit_tests/services/test_human_input_service.py +++ b/api/tests/unit_tests/services/test_human_input_service.py @@ -3,18 +3,18 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock import pytest - -import services.human_input_service as human_input_service_module -from core.repositories.human_input_repository import ( - HumanInputFormRecord, - HumanInputFormSubmissionRepository, -) from graphon.nodes.human_input.entities import ( FormDefinition, FormInput, UserAction, ) from graphon.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus + +import services.human_input_service as human_input_service_module +from core.repositories.human_input_repository import ( + HumanInputFormRecord, + HumanInputFormSubmissionRepository, +) from libs.datetime_utils import naive_utc_now from models.human_input import RecipientType from services.human_input_service import ( diff --git a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py index 97f3bd6f01..1bd979b9ec 100644 --- a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py +++ b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py @@ -1,11 +1,11 @@ import types import pytest - -from core.entities.provider_entities import CredentialConfiguration, CustomModelConfiguration from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ConfigurateMethod + +from core.entities.provider_entities import CredentialConfiguration, CustomModelConfiguration from models.provider import ProviderType from services.model_provider_service import ModelProviderService diff --git a/api/tests/unit_tests/services/test_variable_truncator.py b/api/tests/unit_tests/services/test_variable_truncator.py index 931e96ef3a..148234ac75 100644 --- a/api/tests/unit_tests/services/test_variable_truncator.py +++ b/api/tests/unit_tests/services/test_variable_truncator.py @@ -16,7 +16,6 @@ from typing import Any from uuid import uuid4 import pytest - from graphon.file import File, FileTransferMethod, FileType from graphon.variables.segments import ( ArrayFileSegment, @@ -29,6 +28,7 @@ from graphon.variables.segments import ( ObjectSegment, StringSegment, ) + from services.variable_truncator import ( DummyVariableTruncator, MaxDepthExceededError, diff --git a/api/tests/unit_tests/services/test_workflow_run_service_pause.py b/api/tests/unit_tests/services/test_workflow_run_service_pause.py index 239cc83518..a62c9f4555 100644 --- a/api/tests/unit_tests/services/test_workflow_run_service_pause.py +++ b/api/tests/unit_tests/services/test_workflow_run_service_pause.py @@ -13,10 +13,10 @@ from datetime import datetime from unittest.mock import MagicMock, create_autospec, patch import pytest +from graphon.enums import WorkflowExecutionStatus from sqlalchemy import Engine from sqlalchemy.orm import Session, sessionmaker -from graphon.enums import WorkflowExecutionStatus from models.workflow import WorkflowPause from repositories.api_workflow_run_repository import APIWorkflowRunRepository from repositories.sqlalchemy_api_workflow_run_repository import _PrivateWorkflowPauseEntity diff --git a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py index 497c26a9b3..4e18016123 100644 --- a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py +++ b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py @@ -4,12 +4,12 @@ import json from unittest.mock import Mock, patch import pytest -from sqlalchemy import Engine - -from core.workflow.file_reference import build_file_reference from graphon.file import File, FileTransferMethod, FileType from graphon.variables.segments import ObjectSegment, StringSegment from graphon.variables.types import SegmentType +from sqlalchemy import Engine + +from core.workflow.file_reference import build_file_reference from models.model import UploadFile from models.workflow import WorkflowDraftVariable, WorkflowDraftVariableFile from services.workflow_draft_variable_service import DraftVarLoader diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py index b14d767568..409e8bbddf 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -4,6 +4,10 @@ import uuid from unittest.mock import MagicMock, Mock, patch import pytest +from graphon.enums import BuiltinNodeTypes +from graphon.file import File, FileTransferMethod, FileType +from graphon.variables.segments import StringSegment +from graphon.variables.types import SegmentType from sqlalchemy import Engine from sqlalchemy.orm import Session @@ -13,10 +17,6 @@ from core.workflow.variable_prefixes import ( ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID, ) -from graphon.enums import BuiltinNodeTypes -from graphon.file import File, FileTransferMethod, FileType -from graphon.variables.segments import StringSegment -from graphon.variables.types import SegmentType from libs.uuid_utils import uuidv7 from models.account import Account from models.enums import DraftVariableType diff --git a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py index d570dce107..4146fd312b 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py @@ -6,13 +6,13 @@ from datetime import UTC, datetime from threading import Event import pytest +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState, VariablePool from models.enums import CreatorUserRole from models.model import AppMode from models.workflow import WorkflowRun diff --git a/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py b/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py index 591da56f49..7119217e94 100644 --- a/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py +++ b/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py @@ -5,8 +5,8 @@ from types import SimpleNamespace from typing import Any import pytest - from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus + from tasks import human_input_timeout_tasks as task_module diff --git a/api/tests/unit_tests/tools/test_mcp_tool.py b/api/tests/unit_tests/tools/test_mcp_tool.py index 689b973097..544e89fcee 100644 --- a/api/tests/unit_tests/tools/test_mcp_tool.py +++ b/api/tests/unit_tests/tools/test_mcp_tool.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import Mock, patch import pytest +from graphon.model_runtime.entities.llm_entities import LLMUsage from core.mcp.types import ( AudioContent, @@ -18,7 +19,6 @@ from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage from core.tools.mcp_tool.tool import MCPTool -from graphon.model_runtime.entities.llm_entities import LLMUsage def _make_mcp_tool(output_schema: dict[str, Any] | None = None) -> MCPTool: diff --git a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py index c166a946d9..ffa6833524 100644 --- a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py +++ b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py @@ -2,9 +2,6 @@ from decimal import Decimal from unittest.mock import MagicMock, patch import pytest - -from core.llm_generator.output_parser.errors import OutputParserError -from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output from graphon.model_runtime.entities.llm_entities import ( LLMResult, LLMResultChunk, @@ -21,6 +18,9 @@ from graphon.model_runtime.entities.message_entities import ( ) from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output + def create_mock_usage(prompt_tokens: int = 10, completion_tokens: int = 5) -> LLMUsage: """Create a mock LLMUsage with all required fields""" diff --git a/api/uv.lock b/api/uv.lock index 6360610851..cf54fced75 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -388,14 +388,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.11" +version = "1.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/10/b325d58ffe86815b399334a101e63bc6fa4e1953921cb23703b48a0a0220/authlib-1.6.11.tar.gz", hash = "sha256:64db35b9b01aeccb4715a6c9a6613a06f2bd7be2ab9d2eb89edd1dfc7580a38f", size = 165359, upload-time = "2026-04-16T07:22:50.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/2f/55fca558f925a51db046e5b929deb317ddb05afed74b22d89f4eca578980/authlib-1.6.11-py2.py3-none-any.whl", hash = "sha256:c8687a9a26451c51a34a06fa17bb97cb15bba46a6a626755e2d7f50da8bff3e3", size = 244469, upload-time = "2026-04-16T07:22:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] [[package]] @@ -4055,14 +4055,14 @@ wheels = [ [[package]] name = "mako" -version = "1.3.11" +version = "1.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" }, + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] [[package]] @@ -5653,11 +5653,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.10.2" +version = "6.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/3f/9f2167401c2e94833ca3b69535bad89e533b5de75fefe4197a2c224baec2/pypdf-6.10.2.tar.gz", hash = "sha256:7d09ce108eff6bf67465d461b6ef352dcb8d84f7a91befc02f904455c6eea11d", size = 5315679, upload-time = "2026-04-15T16:37:36.978Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/79/f2730c42ec7891a75a2fcea2eb4f356872bcbc671b711418060424796612/pypdf-6.10.1.tar.gz", hash = "sha256:62e6ca7f65aaa28b3d192addb44f97296e4be1748f57ed0f4efb2d4915841880", size = 5315704, upload-time = "2026-04-14T12:55:20.996Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl", hash = "sha256:aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa", size = 336308, upload-time = "2026-04-15T16:37:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/f0/04/e3aa7f1f14dbc53429cae34666261eb935d99bd61d24756ab94d7e0309da/pypdf-6.10.1-py3-none-any.whl", hash = "sha256:6331940d3bfe75b7e6601d35db7adabab5fc1d716efaeb384e3c0c3957d033de", size = 335606, upload-time = "2026-04-14T12:55:18.941Z" }, ] [[package]] diff --git a/sdks/nodejs-client/tsconfig.json b/sdks/nodejs-client/tsconfig.json index 1e55007ed0..46055447be 100644 --- a/sdks/nodejs-client/tsconfig.json +++ b/sdks/nodejs-client/tsconfig.json @@ -1,14 +1,18 @@ { - "extends": "@dify/tsconfig/node.json", "compilerOptions": { - "lib": ["ES2023", "DOM", "DOM.Iterable"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", "rootDir": ".", "outDir": "dist", - "noEmit": false, "declaration": true, "declarationMap": true, "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, "types": ["node"] }, - "include": ["src/**/*.ts", "tests/**/*.ts", "vite.config.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] } diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index 2814072860..29a08f8a01 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -76,8 +76,8 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type )} {mode === 'expand' && (
-
-
+
+
{name}
{hoverTip @@ -95,10 +95,10 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type )}
{!hideType && isExtraInLine && ( -
{type}
+
{type}
)} {!hideType && !isExtraInLine && ( -
{isExternal ? t('externalTag', { ns: 'dataset' }) : type}
+
{isExternal ? t('externalTag', { ns: 'dataset' }) : type}
)}
)} diff --git a/web/app/components/app-sidebar/dataset-info/menu-item.tsx b/web/app/components/app-sidebar/dataset-info/menu-item.tsx index d426512176..7ad8d9407f 100644 --- a/web/app/components/app-sidebar/dataset-info/menu-item.tsx +++ b/web/app/components/app-sidebar/dataset-info/menu-item.tsx @@ -22,7 +22,7 @@ const MenuItem = ({ }} > - {name} + {name}
) } diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index e47d23f6bf..f476b0f7b5 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -737,4 +737,178 @@ describe('AppPublisher', () => { expect(sectionProps.summary?.workflowTypeSwitchDisabled).toBe(true) expect(sectionProps.summary?.workflowTypeSwitchDisabledReason).toBe('common.switchToEvaluationWorkflowDisabledTip') }) + + it('should switch workflow type, refresh app detail, and close the popover for published apps', async () => { + mockFetchAppDetailDirect.mockResolvedValueOnce({ + id: 'app-1', + type: AppTypeEnum.EVALUATION, + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockConvertWorkflowType).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + query: { target_type: AppTypeEnum.EVALUATION }, + }) + expect(mockFetchAppDetailDirect).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' }) + expect(mockSetAppDetail).toHaveBeenCalledWith({ + id: 'app-1', + type: AppTypeEnum.EVALUATION, + }) + }) + expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument() + }) + + it('should hide access and actions sections for evaluation workflow apps', () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + + expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + expect(screen.queryByText('publisher-access-control')).not.toBeInTheDocument() + expect(screen.queryByText('publisher-embed')).not.toBeInTheDocument() + expect(sectionProps.summary?.workflowTypeSwitchConfig).toEqual({ + targetType: AppTypeEnum.WORKFLOW, + publishLabelKey: 'common.publishAsStandardWorkflow', + switchLabelKey: 'common.switchToStandardWorkflow', + tipKey: 'common.switchToStandardWorkflowTip', + }) + }) + + it('should confirm before switching an evaluation workflow with associated targets to a standard workflow', async () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + mockEvaluationWorkflowAssociatedTargets = { + items: [ + { + target_type: 'app', + target_id: 'dependent-app-1', + target_name: 'Dependent App', + }, + { + target_type: 'knowledge_base', + target_id: 'knowledge-1', + target_name: 'Knowledge Base', + }, + ], + } + mockRefetchEvaluationWorkflowAssociatedTargets.mockResolvedValueOnce({ + data: mockEvaluationWorkflowAssociatedTargets, + isError: false, + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockRefetchEvaluationWorkflowAssociatedTargets).toHaveBeenCalledTimes(1) + }) + expect(mockConvertWorkflowType).not.toHaveBeenCalled() + expect(screen.getByText('Dependent App')).toBeInTheDocument() + expect(screen.getByText('Knowledge Base')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'common.switchToStandardWorkflowConfirm.switch' })) + + await waitFor(() => { + expect(mockConvertWorkflowType).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + query: { target_type: AppTypeEnum.WORKFLOW }, + }) + }) + }) + + it('should switch an evaluation workflow directly when there are no associated targets', async () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockRefetchEvaluationWorkflowAssociatedTargets).toHaveBeenCalledTimes(1) + expect(mockConvertWorkflowType).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + query: { target_type: AppTypeEnum.WORKFLOW }, + }) + }) + expect(screen.queryByText('common.switchToStandardWorkflowConfirm.title')).not.toBeInTheDocument() + }) + + it('should block switching an evaluation workflow when associated targets fail to load', async () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + mockRefetchEvaluationWorkflowAssociatedTargets.mockResolvedValueOnce({ + data: undefined, + isError: true, + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('common.switchToStandardWorkflowConfirm.loadFailed') + }) + expect(mockConvertWorkflowType).not.toHaveBeenCalled() + }) + + it('should block switching to evaluation workflow when restricted nodes exist', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('common.switchToEvaluationWorkflowDisabledTip') + }) + + expect(mockConvertWorkflowType).not.toHaveBeenCalled() + expect(sectionProps.summary?.workflowTypeSwitchDisabled).toBe(true) + expect(sectionProps.summary?.workflowTypeSwitchDisabledReason).toBe('common.switchToEvaluationWorkflowDisabledTip') + }) }) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 2ee209683d..7e4ed89215 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -285,7 +285,7 @@ const AppPublisher = ({ throw new Error('App not found') const { installed_apps } = await fetchInstalledAppList(appDetail.id) if (installed_apps?.length > 0) - return `${basePath}/explore/installed/${installed_apps[0]!.id}` + return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') }, { onError: (err) => { diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 5fd394ad45..6c0eaacc5a 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -96,8 +96,8 @@ const AdvancedPromptInput: FC = ({ }, onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { - if (promptVariables[i]!.key === newExternalDataTool.variable) { - toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i]!.key })) + if (promptVariables[i].key === newExternalDataTool.variable) { + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 2935504f15..17ec28d795 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -94,8 +94,8 @@ const Prompt: FC = ({ }, onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { - if (promptVariables[i]!.key === newExternalDataTool.variable) { - toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i]!.key })) + if (promptVariables[i].key === newExternalDataTool.variable) { + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 34c6bf5786..8997eedeac 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -165,8 +165,8 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar }, onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { - if (promptVariables[i]!.key === newExternalDataTool.variable && i !== index) { - toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i]!.key })) + if (promptVariables[i].key === newExternalDataTool.variable && i !== index) { + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -220,7 +220,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar const handleRemoveVar = useCallback((index: number) => { const removeVar = promptVariables[index] - if (mode === AppModeEnum.COMPLETION && dataSets.length > 0 && removeVar!.is_context_var) { + if (mode === AppModeEnum.COMPLETION && dataSets.length > 0 && removeVar.is_context_var) { showDeleteContextVarModal() setRemoveIndex(index) return diff --git a/web/app/components/base/audio-gallery/AudioPlayer.tsx b/web/app/components/base/audio-gallery/AudioPlayer.tsx index 1290fff871..d7865df5e7 100644 --- a/web/app/components/base/audio-gallery/AudioPlayer.tsx +++ b/web/app/components/base/audio-gallery/AudioPlayer.tsx @@ -95,7 +95,7 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { for (let i = 0; i < samples; i++) { let sum = 0 for (let j = 0; j < blockSize; j++) - sum += Math.abs(channelData[i * blockSize + j]!) + sum += Math.abs(channelData[i * blockSize + j]) // Apply nonlinear scaling to enhance small amplitudes waveformData.push((sum / blockSize) * 5) } @@ -145,7 +145,7 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { e.preventDefault() const getClientX = (event: React.MouseEvent | React.TouchEvent): number => { if ('touches' in event) - return event.touches[0]!.clientX + return event.touches[0].clientX return event.clientX } const updateProgress = (clientX: number) => { diff --git a/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx index 83a8666e79..bd5f01bcda 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx @@ -151,8 +151,8 @@ describe('ChatWrapper', () => { render() - expect(await screen.findByText('Welcome'))!.toBeInTheDocument() - expect(await screen.findByText('Q1'))!.toBeInTheDocument() + expect(await screen.findByText('Welcome')).toBeInTheDocument() + expect(await screen.findByText('Q1')).toBeInTheDocument() fireEvent.click(screen.getByText('Q1')) expect(handleSend).toHaveBeenCalled() @@ -170,7 +170,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Default opening statement'))!.toBeInTheDocument() + expect(screen.getByText('Default opening statement')).toBeInTheDocument() }) it('should render welcome screen without suggested questions', async () => { @@ -186,7 +186,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Welcome message'))!.toBeInTheDocument() + expect(await screen.findByText('Welcome message')).toBeInTheDocument() }) it('should show responding state', async () => { @@ -197,7 +197,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Bot thinking...'))!.toBeInTheDocument() + expect(await screen.findByText('Bot thinking...')).toBeInTheDocument() }) it('should handle manual message input and stop responding', async () => { @@ -320,9 +320,9 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const disabledContainer = chatInput!.closest('.pointer-events-none') - expect(disabledContainer)!.toBeInTheDocument() - expect(disabledContainer)!.toHaveClass('opacity-50') + const disabledContainer = chatInput.closest('.pointer-events-none') + expect(disabledContainer).toBeInTheDocument() + expect(disabledContainer).toHaveClass('opacity-50') }) it('should not disable input when required field has value', () => { @@ -337,7 +337,7 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') + const container = chatInput.closest('.pointer-events-none') expect(container).not.toBeInTheDocument() }) @@ -361,8 +361,8 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') - expect(container)!.toBeInTheDocument() + const container = chatInput.closest('.pointer-events-none') + expect(container).toBeInTheDocument() }) it('should not disable input when file is fully uploaded', () => { @@ -411,8 +411,8 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') - expect(container)!.toBeInTheDocument() + const container = chatInput.closest('.pointer-events-none') + expect(container).toBeInTheDocument() }) it('should not disable when all files are uploaded', () => { @@ -457,7 +457,7 @@ describe('ChatWrapper', () => { render() const textarea = screen.getByRole('textbox') const container = textarea.closest('.pointer-events-none') - expect(container)!.toBeInTheDocument() + expect(container).toBeInTheDocument() }) it('should not disable input when allInputsHidden is true', () => { @@ -523,7 +523,7 @@ describe('ChatWrapper', () => { render() expect(handleSwitchSibling).toHaveBeenCalledWith('resume-node', expect.any(Object)) - const resumeOptions = handleSwitchSibling.mock.calls[0]![1] + const resumeOptions = handleSwitchSibling.mock.calls[0][1] resumeOptions.onGetSuggestedQuestions('response-from-resume') expect(fetchSuggestedQuestions).toHaveBeenCalledWith('response-from-resume', 'webApp', 'test-app-id') }) @@ -619,7 +619,7 @@ describe('ChatWrapper', () => { render() - const onStopCallback = vi.mocked(useChat).mock.calls[0]![3] as (taskId: string) => void + const onStopCallback = vi.mocked(useChat).mock.calls[0][3] as (taskId: string) => void onStopCallback('taskId-123') expect(stopChatMessageResponding).toHaveBeenCalledWith('', 'taskId-123', 'webApp', 'test-app-id') }) @@ -645,7 +645,7 @@ describe('ChatWrapper', () => { expect(handleSend).toHaveBeenCalled() // Get the options passed to handleSend - const options = handleSend.mock.calls[0]![2] + const options = handleSend.mock.calls[0][2] expect(options.isPublicAPI).toBe(true) // Call onGetSuggestedQuestions @@ -679,7 +679,7 @@ describe('ChatWrapper', () => { fireEvent.click(nextButton) expect(handleSwitchSibling).toHaveBeenCalled() - const options = handleSwitchSibling.mock.calls[0]![1] + const options = handleSwitchSibling.mock.calls[0][1] options.onGetSuggestedQuestions('response-id') expect(fetchSuggestedQuestions).toHaveBeenCalledWith('response-id', 'webApp', 'test-app-id') } @@ -708,8 +708,8 @@ describe('ChatWrapper', () => { expect(handleSend).toHaveBeenCalled() const args = handleSend.mock.calls[0] // args[1] is data - expect(args![1].query).toBe('Q1') - expect(args![1].parent_message_id).toBeNull() + expect(args[1].query).toBe('Q1') + expect(args[1].parent_message_id).toBeNull() } }) @@ -737,7 +737,7 @@ describe('ChatWrapper', () => { fireEvent.click(regenerateBtn) expect(handleSend).toHaveBeenCalled() const args = handleSend.mock.calls[0] - expect(args![1].parent_message_id).toBe('a0') + expect(args[1].parent_message_id).toBe('a0') } }) @@ -774,10 +774,10 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Node 1'))!.toBeInTheDocument() + expect(await screen.findByText('Node 1')).toBeInTheDocument() const input = screen.getAllByRole('textbox').find(el => el.closest('.chat-answer-container')) || screen.getAllByRole('textbox')[0] - fireEvent.change(input!, { target: { value: 'test' } }) + fireEvent.change(input, { target: { value: 'test' } }) const runButton = screen.getByText('Run') fireEvent.click(runButton) @@ -817,10 +817,10 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Node Web 1'))!.toBeInTheDocument() + expect(await screen.findByText('Node Web 1')).toBeInTheDocument() const input = screen.getAllByRole('textbox').find(el => el.closest('.chat-answer-container')) || screen.getAllByRole('textbox')[0] - fireEvent.change(input!, { target: { value: 'web-test' } }) + fireEvent.change(input, { target: { value: 'web-test' } }) fireEvent.click(screen.getByText('Run')) await waitFor(() => { @@ -841,7 +841,7 @@ describe('ChatWrapper', () => { render() expect(document.querySelector('.chat-answer-container')).not.toBeInTheDocument() - expect(screen.getByText('Welcome'))!.toBeInTheDocument() + expect(screen.getByText('Welcome')).toBeInTheDocument() }) it('should show all messages including opening statement when there are multiple messages', () => { @@ -861,7 +861,7 @@ describe('ChatWrapper', () => { render() const welcomeElements = screen.getAllByText('Welcome') expect(welcomeElements.length).toBeGreaterThan(0) - expect(screen.getByText('User message'))!.toBeInTheDocument() + expect(screen.getByText('User message')).toBeInTheDocument() }) it('should show chatNode and inputs form on desktop for new conversation', () => { @@ -873,7 +873,7 @@ describe('ChatWrapper', () => { }) render() - expect(screen.getByText('Test'))!.toBeInTheDocument() + expect(screen.getByText('Test')).toBeInTheDocument() }) it('should show chatNode on mobile for new conversation only', () => { @@ -885,7 +885,7 @@ describe('ChatWrapper', () => { }) const { rerender } = render() - expect(screen.getByText('Test'))!.toBeInTheDocument() + expect(screen.getByText('Test')).toBeInTheDocument() vi.mocked(useChatWithHistoryContext).mockReturnValue({ ...defaultContextValue, @@ -974,8 +974,8 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Answer'))!.toBeInTheDocument() - expect(screen.getByAltText('answer icon'))!.toBeInTheDocument() + expect(screen.getByText('Answer')).toBeInTheDocument() + expect(screen.getByAltText('answer icon')).toBeInTheDocument() }) it('should render question icon fallback when user avatar is available', () => { @@ -993,7 +993,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('J'))!.toBeInTheDocument() + expect(screen.getByText('J')).toBeInTheDocument() }) it('should use fallback values for nullable appData, appMeta and avatar name', () => { @@ -1012,8 +1012,8 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Question with fallback avatar name'))!.toBeInTheDocument() - expect(screen.getByText('U'))!.toBeInTheDocument() + expect(screen.getByText('Question with fallback avatar name')).toBeInTheDocument() + expect(screen.getByText('U')).toBeInTheDocument() }) it('should set handleStop on currentChatInstanceRef', () => { @@ -1101,8 +1101,8 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') - expect(container)!.toBeInTheDocument() + const container = chatInput.closest('.pointer-events-none') + expect(container).toBeInTheDocument() }) it('should call formatBooleanInputs when sending message', async () => { @@ -1223,8 +1223,7 @@ describe('ChatWrapper', () => { render() // This tests line 91 - using currentConversationItem.introduction - // This tests line 91 - using currentConversationItem.introduction - expect(screen.getByText('Custom introduction from conversation item'))!.toBeInTheDocument() + expect(screen.getByText('Custom introduction from conversation item')).toBeInTheDocument() }) it('should handle early return when hasEmptyInput is already set', () => { @@ -1243,8 +1242,8 @@ describe('ChatWrapper', () => { // This tests line 106 - early return when hasEmptyInput is set const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') - expect(container)!.toBeInTheDocument() + const container = chatInput.closest('.pointer-events-none') + expect(container).toBeInTheDocument() }) it('should handle early return when fileIsUploading is already set', () => { @@ -1271,8 +1270,8 @@ describe('ChatWrapper', () => { // This tests line 109 - early return when fileIsUploading is set const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') - expect(container)!.toBeInTheDocument() + const container = chatInput.closest('.pointer-events-none') + expect(container).toBeInTheDocument() }) it('should handle doSend with no parent message id', async () => { @@ -1562,7 +1561,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Default opening statement'))!.toBeInTheDocument() + expect(screen.getByText('Default opening statement')).toBeInTheDocument() }) it('should handle doSend when regenerating with null parentAnswer', async () => { @@ -1610,9 +1609,7 @@ describe('ChatWrapper', () => { // Just verify the component renders - the actual editedQuestion flow // is tested through the doRegenerate callback that's passed to Chat - // Just verify the component renders - the actual editedQuestion flow - // is tested through the doRegenerate callback that's passed to Chat - expect(screen.getByText('Answer'))!.toBeInTheDocument() + expect(screen.getByText('Answer')).toBeInTheDocument() expect(handleSend).toBeDefined() }) @@ -1632,9 +1629,7 @@ describe('ChatWrapper', () => { // The doRegenerate is passed to Chat component and would be called // This ensures lines 198-200 are covered - // The doRegenerate is passed to Chat component and would be called - // This ensures lines 198-200 are covered - expect(screen.getByText('A1'))!.toBeInTheDocument() + expect(screen.getByText('A1')).toBeInTheDocument() }) it('should handle doRegenerate when question has message_files', async () => { @@ -1814,38 +1809,7 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput!.closest('.pointer-events-none') - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required - // Should not be disabled because it's not required + const container = chatInput.closest('.pointer-events-none') // Should not be disabled because it's not required expect(container).not.toBeInTheDocument() }) diff --git a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx index b1c23a129b..5feaccd191 100644 --- a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx @@ -108,7 +108,7 @@ describe('Header Component', () => { currentConversationItem: mockConv, sidebarCollapseState: true, }) - expect(screen.getByText('My Chat'))!.toBeInTheDocument() + expect(screen.getByText('My Chat')).toBeInTheDocument() }) it('should render ViewFormDropdown trigger when inputsForms are present', () => { @@ -133,7 +133,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') // Sidebar, NewChat, ResetChat (3) const resetChatBtn = buttons[buttons.length - 1] - await userEvent.click(resetChatBtn!) + await userEvent.click(resetChatBtn) expect(handleNewConversation).toHaveBeenCalled() }) @@ -144,7 +144,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') const sidebarBtn = buttons[0] - await userEvent.click(sidebarBtn!) + await userEvent.click(sidebarBtn) expect(handleSidebarCollapse).toHaveBeenCalledWith(false) }) @@ -163,7 +163,7 @@ describe('Header Component', () => { await userEvent.click(trigger) const pinBtn = await screen.findByText('explore.sidebar.action.pin') - expect(pinBtn)!.toBeInTheDocument() + expect(pinBtn).toBeInTheDocument() await userEvent.click(pinBtn) @@ -225,7 +225,7 @@ describe('Header Component', () => { const renameMenuBtn = await screen.findByText('explore.sidebar.action.rename') await userEvent.click(renameMenuBtn) - expect(await screen.findByText('common.chat.renameConversation'))!.toBeInTheDocument() + expect(await screen.findByText('common.chat.renameConversation')).toBeInTheDocument() const input = screen.getByDisplayValue('My Chat') await userEvent.clear(input) @@ -236,7 +236,7 @@ describe('Header Component', () => { expect(handleRenameConversation).toHaveBeenCalledWith('conv-1', 'New Name', expect.any(Object)) - const successCallback = handleRenameConversation.mock.calls[0]![2].onSuccess + const successCallback = handleRenameConversation.mock.calls[0][2].onSuccess await act(async () => { successCallback() }) @@ -262,14 +262,14 @@ describe('Header Component', () => { await userEvent.click(deleteMenuBtn) expect(handleDeleteConversation).not.toHaveBeenCalled() - expect(await screen.findByText('share.chat.deleteConversation.title'))!.toBeInTheDocument() + expect(await screen.findByText('share.chat.deleteConversation.title')).toBeInTheDocument() const confirmBtn = await screen.findByText('common.operation.confirm') await userEvent.click(confirmBtn) expect(handleDeleteConversation).toHaveBeenCalledWith('conv-1', expect.any(Object)) - const successCallback = handleDeleteConversation.mock.calls[0]![1].onSuccess + const successCallback = handleDeleteConversation.mock.calls[0][1].onSuccess await act(async () => { successCallback() }) @@ -311,7 +311,7 @@ describe('Header Component', () => { await userEvent.click(screen.getByText('My Chat')) await userEvent.click(await screen.findByText('explore.sidebar.action.delete')) - expect(await screen.findByText('share.chat.deleteConversation.title'))!.toBeInTheDocument() + expect(await screen.findByText('share.chat.deleteConversation.title')).toBeInTheDocument() }) }) @@ -332,7 +332,7 @@ describe('Header Component', () => { it('should render system title if conversation id is missing', () => { setup({ currentConversationId: '', sidebarCollapseState: true }) const titleEl = screen.getByText('Test App') - expect(titleEl)!.toHaveClass('system-md-semibold') + expect(titleEl).toHaveClass('system-md-semibold') }) it('should render app icon from URL when icon_url is provided', () => { @@ -347,7 +347,7 @@ describe('Header Component', () => { }, }) const img = screen.getByAltText('app icon') - expect(img)!.toHaveAttribute('src', 'https://example.com/icon.png') + expect(img).toHaveAttribute('src', 'https://example.com/icon.png') }) it('should handle undefined appData gracefully (optional chaining)', () => { @@ -364,8 +364,7 @@ describe('Header Component', () => { sidebarCollapseState: true, }) // The separator is just a div with text content '/' - // The separator is just a div with text content '/' - expect(screen.getByText('/'))!.toBeInTheDocument() + expect(screen.getByText('/')).toBeInTheDocument() }) it('should handle New Chat button state when currentConversationId is present but isResponding is true', () => { @@ -378,7 +377,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') // Sidebar, NewChat, ResetChat (3) const newChatBtn = buttons[1] - expect(newChatBtn)!.toBeDisabled() + expect(newChatBtn).toBeDisabled() }) it('should handle New Chat button state when currentConversationId is missing and isResponding is false', () => { @@ -391,7 +390,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') // Sidebar, NewChat (2) const newChatBtn = buttons[1] - expect(newChatBtn)!.toBeDisabled() + expect(newChatBtn).toBeDisabled() }) it('should not render operation menu if conversation id is missing', () => { diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 223c35f6dd..55cc303a45 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -452,7 +452,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) setOriginConversationList(produce((draft) => { const index = originConversationList.findIndex(item => item.id === conversationId) - const item = draft[index]! + const item = draft[index] draft[index] = { ...item, name: newName, diff --git a/web/app/components/base/chat/chat/citation/popup.tsx b/web/app/components/base/chat/chat/citation/popup.tsx index 51a73bc4b6..2b4070b69a 100644 --- a/web/app/components/base/chat/chat/citation/popup.tsx +++ b/web/app/components/base/chat/chat/citation/popup.tsx @@ -64,10 +64,10 @@ const Popup: FC = ({
-
+
-
+
{(data.dataSourceType === 'upload_file' || data.dataSourceType === 'file') && !!data.sources?.[0]?.dataset_id ? ( ) - expect(screen.getByRole('button', { name: 'Run' }))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Run' })).toBeInTheDocument() }) it('should render correctly without optional className props', () => { const { wrapper, canvasLayer, gradientLayer, contentLayer } = renderGridMask({}, Plain child) - expect(wrapper)!.toHaveClass('bg-saas-background') - expect(canvasLayer)!.toHaveClass('absolute') - expect(gradientLayer)!.toHaveClass('absolute') - expect(contentLayer)!.toHaveTextContent('Plain child') + expect(wrapper).toHaveClass('bg-saas-background') + expect(canvasLayer).toHaveClass('absolute') + expect(gradientLayer).toHaveClass('absolute') + expect(contentLayer).toHaveTextContent('Plain child') }) it('should render wrapper, canvas, gradient and content layers in order', () => { const { wrapper, canvasLayer, gradientLayer, contentLayer } = renderGridMask({}, Content) - expect(wrapper)!.toBeInTheDocument() + expect(wrapper).toBeInTheDocument() expect(wrapper.children).toHaveLength(3) - expect(canvasLayer)!.toHaveClass('z-0') - expect(gradientLayer)!.toHaveClass('z-1') - expect(contentLayer)!.toHaveClass('z-2') - expect(contentLayer)!.toHaveTextContent('Content') + expect(canvasLayer).toHaveClass('z-0') + expect(gradientLayer).toHaveClass('z-1') + expect(contentLayer).toHaveClass('z-2') + expect(contentLayer).toHaveTextContent('Content') }) }) describe('Props', () => { it('should apply wrapperClassName to wrapper element', () => { const { wrapper } = renderGridMask({ wrapperClassName: 'custom-wrapper' }, Child) - expect(wrapper)!.toHaveClass('custom-wrapper') - expect(wrapper)!.toHaveClass('relative') + expect(wrapper).toHaveClass('custom-wrapper') + expect(wrapper).toHaveClass('relative') }) it('should apply canvasClassName and grid background class to canvas layer', () => { const { canvasLayer } = renderGridMask({ canvasClassName: 'custom-canvas' }, Child) - expect(canvasLayer)!.toHaveClass('custom-canvas') - expect(canvasLayer)!.toHaveClass(Style.gridBg!) + expect(canvasLayer).toHaveClass('custom-canvas') + expect(canvasLayer).toHaveClass(Style.gridBg) }) it('should apply gradientClassName to gradient layer', () => { const { gradientLayer } = renderGridMask({ gradientClassName: 'custom-gradient' }, Child) - expect(gradientLayer)!.toHaveClass('custom-gradient') - expect(gradientLayer)!.toHaveClass('bg-grid-mask-background') + expect(gradientLayer).toHaveClass('custom-gradient') + expect(gradientLayer).toHaveClass('bg-grid-mask-background') }) }) }) diff --git a/web/app/components/base/image-uploader/hooks.ts b/web/app/components/base/image-uploader/hooks.ts index 37cd5b5995..beba92951e 100644 --- a/web/app/components/base/image-uploader/hooks.ts +++ b/web/app/components/base/image-uploader/hooks.ts @@ -31,7 +31,7 @@ export const useImageFiles = () => { const files = filesRef.current const index = files.findIndex(file => file._id === imageFileId) if (index > -1) { - const currentFile = files[index]! + const currentFile = files[index] const newFiles = [...files.slice(0, index), { ...currentFile, deleted: true }, ...files.slice(index + 1)] setFiles(newFiles) filesRef.current = newFiles @@ -41,7 +41,7 @@ export const useImageFiles = () => { const files = filesRef.current const index = files.findIndex(file => file._id === imageFileId) if (index > -1) { - const currentFile = files[index]! + const currentFile = files[index] const newFiles = [...files.slice(0, index), { ...currentFile, progress: -1 }, ...files.slice(index + 1)] filesRef.current = newFiles setFiles(newFiles) @@ -51,7 +51,7 @@ export const useImageFiles = () => { const files = filesRef.current const index = files.findIndex(file => file._id === imageFileId) if (index > -1) { - const currentImageFile = files[index]! + const currentImageFile = files[index] const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: 100 }, ...files.slice(index + 1)] filesRef.current = newFiles setFiles(newFiles) @@ -61,9 +61,9 @@ export const useImageFiles = () => { const files = filesRef.current const index = files.findIndex(file => file._id === imageFileId) if (index > -1) { - const currentImageFile = files[index]! + const currentImageFile = files[index] imageUpload({ - file: currentImageFile!.file!, + file: currentImageFile.file!, onProgressCallback: (progress) => { const newFiles = [...files.slice(0, index), { ...currentImageFile, progress }, ...files.slice(index + 1)] filesRef.current = newFiles @@ -114,7 +114,7 @@ export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useL // TODO: leave some warnings? return } - if (!ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1]!)) + if (!ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1])) return if (limit && file.size > limit * 1024 * 1024) { toast.error(t('imageUploader.uploadFromComputerLimit', { ns: 'common', size: limit })) diff --git a/web/app/components/base/image-uploader/image-list.stories.tsx b/web/app/components/base/image-uploader/image-list.stories.tsx index 0c27211d16..cfea4a0da0 100644 --- a/web/app/components/base/image-uploader/image-list.stories.tsx +++ b/web/app/components/base/image-uploader/image-list.stories.tsx @@ -132,7 +132,7 @@ const ImageUploaderPlayground = ({ readonly }: Story['args']) => { return (
- Add images + Add images
)} diff --git a/web/app/components/base/notion-icon/index.tsx b/web/app/components/base/notion-icon/index.tsx index f2b5146d73..62fcef1dc1 100644 --- a/web/app/components/base/notion-icon/index.tsx +++ b/web/app/components/base/notion-icon/index.tsx @@ -31,7 +31,7 @@ const NotionIcon = ({ ) } return ( -
{name?.[0]!.toLocaleUpperCase()}
+
{name?.[0].toLocaleUpperCase()}
) } diff --git a/web/app/components/base/notion-page-selector/page-selector/__tests__/index.spec.tsx b/web/app/components/base/notion-page-selector/page-selector/__tests__/index.spec.tsx index 21a7a08d63..d4b559452e 100644 --- a/web/app/components/base/notion-page-selector/page-selector/__tests__/index.spec.tsx +++ b/web/app/components/base/notion-page-selector/page-selector/__tests__/index.spec.tsx @@ -24,11 +24,11 @@ const mockList: DataSourceNotionPage[] = [ ] const mockPagesMap: DataSourceNotionPageMap = { - 'root-1': { ...mockList[0]!, workspace_id: 'workspace-1' }, - 'child-1': { ...mockList[1]!, workspace_id: 'workspace-1' }, - 'grandchild-1': { ...mockList[2]!, workspace_id: 'workspace-1' }, - 'child-2': { ...mockList[3]!, workspace_id: 'workspace-1' }, - 'root-2': { ...mockList[4]!, workspace_id: 'workspace-1' }, + 'root-1': { ...mockList[0], workspace_id: 'workspace-1' }, + 'child-1': { ...mockList[1], workspace_id: 'workspace-1' }, + 'grandchild-1': { ...mockList[2], workspace_id: 'workspace-1' }, + 'child-2': { ...mockList[3], workspace_id: 'workspace-1' }, + 'root-2': { ...mockList[4], workspace_id: 'workspace-1' }, } describe('PageSelector', () => { @@ -39,7 +39,7 @@ describe('PageSelector', () => { it('should render root level pages initially', () => { render() - expect(screen.getByText('Root 1'))!.toBeInTheDocument() + expect(screen.getByText('Root 1')).toBeInTheDocument() expect(screen.queryByText('Child 1')).not.toBeInTheDocument() }) @@ -50,13 +50,13 @@ describe('PageSelector', () => { const toggle = screen.getByTestId('notion-page-toggle-root-1') await user.click(toggle) - expect(screen.getByText('Child 1'))!.toBeInTheDocument() + expect(screen.getByText('Child 1')).toBeInTheDocument() }) it('should call onSelect with descendants when parent is selected', async () => { const handleSelect = vi.fn() const user = userEvent.setup() - render() + render() const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-root-1') await user.click(checkbox) @@ -78,7 +78,7 @@ describe('PageSelector', () => { it('should show breadcrumbs when searching', () => { render() - expect(screen.getByText('Root 1 / Child 1 / Grandchild 1'))!.toBeInTheDocument() + expect(screen.getByText('Root 1 / Child 1 / Grandchild 1')).toBeInTheDocument() }) it('should call onPreview when preview button is clicked', async () => { @@ -95,7 +95,7 @@ describe('PageSelector', () => { it('should show no result message when search returns nothing', () => { render() - expect(screen.getByText('common.dataSource.notion.selector.noSearchResult'))!.toBeInTheDocument() + expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument() }) it('should handle selection when searchValue is present', async () => { @@ -124,7 +124,7 @@ describe('PageSelector', () => { const toggleBtn = screen.getByTestId('notion-page-toggle-root-1') await user.click(toggleBtn) // Expand - await waitFor(() => expect(screen.queryByText('Child 1'))!.toBeInTheDocument()) + await waitFor(() => expect(screen.queryByText('Child 1')).toBeInTheDocument()) await user.click(toggleBtn) // Collapse await waitFor(() => expect(screen.queryByText('Child 1')).not.toBeInTheDocument()) @@ -149,14 +149,14 @@ describe('PageSelector', () => { it('should render preview button when canPreview is true', () => { render() - expect(screen.getByTestId('notion-page-preview-root-1'))!.toBeInTheDocument() + expect(screen.getByTestId('notion-page-preview-root-1')).toBeInTheDocument() }) it('should use previewPageId prop when provided', () => { const { rerender } = render() let row = screen.getByTestId('notion-page-row-root-1') - expect(row)!.toHaveClass('bg-state-base-hover') + expect(row).toHaveClass('bg-state-base-hover') rerender() @@ -190,9 +190,8 @@ describe('PageSelector', () => { await user.click(toggle) // Both children should be visible - // Both children should be visible - expect(screen.getByText('Child 1'))!.toBeInTheDocument() - expect(screen.getByText('Child 2'))!.toBeInTheDocument() + expect(screen.getByText('Child 1')).toBeInTheDocument() + expect(screen.getByText('Child 2')).toBeInTheDocument() }) it('should expand nested children when toggling parent', async () => { @@ -202,12 +201,12 @@ describe('PageSelector', () => { // Expand root-1 let toggle = screen.getByTestId('notion-page-toggle-root-1') await user.click(toggle) - expect(screen.getByText('Child 1'))!.toBeInTheDocument() + expect(screen.getByText('Child 1')).toBeInTheDocument() // Expand child-1 toggle = screen.getByTestId('notion-page-toggle-child-1') await user.click(toggle) - expect(screen.getByText('Grandchild 1'))!.toBeInTheDocument() + expect(screen.getByText('Grandchild 1')).toBeInTheDocument() // Collapse child-1 await user.click(toggle) @@ -228,7 +227,7 @@ describe('PageSelector', () => { it('should only select the item when searching (no descendants)', async () => { const handleSelect = vi.fn() const user = userEvent.setup() - render() + render() const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-child-1') await user.click(checkbox) @@ -240,7 +239,7 @@ describe('PageSelector', () => { it('should deselect only the item when searching (no descendants)', async () => { const handleSelect = vi.fn() const user = userEvent.setup() - render() + render() const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-child-1') await user.click(checkbox) @@ -251,8 +250,8 @@ describe('PageSelector', () => { it('should handle multiple root pages', async () => { render() - expect(screen.getByText('Root 1'))!.toBeInTheDocument() - expect(screen.getByText('Root 2'))!.toBeInTheDocument() + expect(screen.getByText('Root 1')).toBeInTheDocument() + expect(screen.getByText('Root 2')).toBeInTheDocument() }) it('should update preview when clicking preview button with onPreview provided', async () => { @@ -277,61 +276,29 @@ describe('PageSelector', () => { rerender() const row = screen.getByTestId('notion-page-row-root-1') - expect(row)!.toHaveClass('bg-state-base-hover') + expect(row).toHaveClass('bg-state-base-hover') }) it('should render page name with correct title attribute', () => { render() const pageName = screen.getByTestId('notion-page-name-root-1') - expect(pageName)!.toHaveAttribute('title', 'Root 1') + expect(pageName).toHaveAttribute('title', 'Root 1') }) it('should handle empty list gracefully', () => { render() - expect(screen.getByText('common.dataSource.notion.selector.noSearchResult'))!.toBeInTheDocument() + expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument() }) it('should filter search results correctly with partial matches', () => { render() // Should show Root 1, Child 1, and Grandchild 1 - // Should show Root 1, Child 1, and Grandchild 1 - expect(screen.getByTestId('notion-page-name-root-1'))!.toBeInTheDocument() - expect(screen.getByTestId('notion-page-name-child-1'))!.toBeInTheDocument() - expect(screen.getByTestId('notion-page-name-grandchild-1'))!.toBeInTheDocument() - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 - // Should not show Root 2, Child 2 + expect(screen.getByTestId('notion-page-name-root-1')).toBeInTheDocument() + expect(screen.getByTestId('notion-page-name-child-1')).toBeInTheDocument() + expect(screen.getByTestId('notion-page-name-grandchild-1')).toBeInTheDocument() // Should not show Root 2, Child 2 expect(screen.queryByTestId('notion-page-name-root-2')).not.toBeInTheDocument() expect(screen.queryByTestId('notion-page-name-child-2')).not.toBeInTheDocument() @@ -346,7 +313,6 @@ describe('PageSelector', () => { await user.click(toggle) // Should expand even though parent is disabled - // Should expand even though parent is disabled - expect(screen.getByText('Child 1'))!.toBeInTheDocument() + expect(screen.getByText('Child 1')).toBeInTheDocument() }) }) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx index 1499fc1d7f..a36403b898 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx @@ -23,7 +23,7 @@ export const PromptMenuItem = memo(({ className={` flex h-6 cursor-pointer items-center rounded-md px-3 hover:bg-state-base-hover ${isSelected && !disabled && 'bg-state-base-hover!'} - ${disabled ? 'cursor-not-allowed opacity-30' : ''} + ${disabled ? 'cursor-not-allowed opacity-30' : 'cursor-pointer hover:bg-state-base-hover'} `} tabIndex={-1} ref={setRefElement} diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx index ee82595d1c..f219f2f805 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx @@ -100,8 +100,8 @@ describe('HITLInputComponent', () => { await user.click(screen.getByRole('button', { name: 'emit-same-name' })) expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange.mock.calls[0]![0]).toHaveLength(1) - expect(onChange.mock.calls[0]![0][0].output_variable_name).toBe('user_name') + expect(onChange.mock.calls[0][0]).toHaveLength(1) + expect(onChange.mock.calls[0][0][0].output_variable_name).toBe('user_name') }) it('should replace payload when variable name is renamed', async () => { @@ -124,7 +124,7 @@ describe('HITLInputComponent', () => { await user.click(screen.getByRole('button', { name: 'emit-rename' })) expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange.mock.calls[0]![0][0].output_variable_name).toBe('renamed_name') + expect(onChange.mock.calls[0][0][0].output_variable_name).toBe('renamed_name') }) it('should update existing payload when variable name stays the same', async () => { @@ -157,9 +157,9 @@ describe('HITLInputComponent', () => { await user.click(screen.getByRole('button', { name: 'emit-update' })) expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange.mock.calls[0]![0][0].default.value).toBe('updated') - expect(onChange.mock.calls[0]![0][0].output_variable_name).toBe('user_name') - expect(onChange.mock.calls[0]![0][1].output_variable_name).toBe('other_name') - expect(onChange.mock.calls[0]![0][1].default.value).toBe('other') + expect(onChange.mock.calls[0][0][0].default.value).toBe('updated') + expect(onChange.mock.calls[0][0][0].output_variable_name).toBe('user_name') + expect(onChange.mock.calls[0][0][1].output_variable_name).toBe('other_name') + expect(onChange.mock.calls[0][0][1].default.value).toBe('other') }) }) diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/pre-populate.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/pre-populate.spec.tsx index 990a7ced4a..f5efc52c23 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/pre-populate.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/pre-populate.spec.tsx @@ -82,12 +82,12 @@ describe('PrePopulate', () => { />, ) - expect(screen.getByText('Static Content'))!.toBeInTheDocument() + expect(screen.getByText('Static Content')).toBeInTheDocument() await user.keyboard('{Tab}') expect(screen.queryByText('Static Content')).not.toBeInTheDocument() - expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should update constant value and toggle to variable mode when type switch is clicked', async () => { @@ -154,7 +154,7 @@ describe('PrePopulate', () => { />, ) - const pickerProps = mockVarReferencePicker.mock.calls[0]![0] as VarReferencePickerProps + const pickerProps = mockVarReferencePicker.mock.calls[0][0] as VarReferencePickerProps const allowString = pickerProps.filterVar({ type: 'string' } as Var) const allowNumber = pickerProps.filterVar({ type: 'number' } as Var) diff --git a/web/app/components/base/prompt-editor/plugins/query-block/component.tsx b/web/app/components/base/prompt-editor/plugins/query-block/component.tsx index cd5b60bc9b..a5b5969904 100644 --- a/web/app/components/base/prompt-editor/plugins/query-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/query-block/component.tsx @@ -17,7 +17,7 @@ const QueryBlockComponent: FC = ({ return (
{ />, ) - expect(screen.getByRole('button', { name: 'label' }))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'label' })).toBeInTheDocument() expect(mockHasNodes).toHaveBeenCalledWith([WorkflowVariableBlockNode]) expect(mockRegisterCommand).toHaveBeenCalledWith( UPDATE_WORKFLOW_NODES_MAP, @@ -188,7 +188,7 @@ describe('WorkflowVariableBlockComponent', () => { />, ) - expect(screen.getByRole('button', { name: 'label' }))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'label' })).toBeInTheDocument() }) it('should pass computed varType when getVarType is provided', () => { @@ -489,7 +489,7 @@ describe('WorkflowVariableBlockComponent', () => { />, ) - const updateHandler = mockRegisterCommand.mock.calls[0]![1] as (payload: UpdateWorkflowNodesMapPayload) => boolean + const updateHandler = mockRegisterCommand.mock.calls[0][1] as (payload: UpdateWorkflowNodesMapPayload) => boolean let result = false act(() => { result = updateHandler({ diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx index 20fc7c6e79..2d13627b20 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -19,11 +19,11 @@ export class WorkflowVariableBlockNode extends DecoratorNode __getVarType?: GetVarType __availableVariables?: NodeOutPutVar[] - static override getType(): string { + static getType(): string { return 'workflow-variable-block' } - static override clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode { + static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode { return new WorkflowVariableBlockNode( node.__variables, node.__workflowNodesMap, @@ -33,7 +33,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode ) } - override isInline(): boolean { + isInline(): boolean { return true } @@ -52,17 +52,17 @@ export class WorkflowVariableBlockNode extends DecoratorNode this.__availableVariables = availableVariables } - override createDOM(): HTMLElement { + createDOM(): HTMLElement { const div = document.createElement('div') div.classList.add('inline-flex', 'items-center', 'align-middle') return div } - override updateDOM(): false { + updateDOM(): false { return false } - override decorate(): React.JSX.Element { + decorate(): React.JSX.Element { return ( ) } - static override importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode { + static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode { const node = $createWorkflowVariableBlockNode( serializedNode.variables, serializedNode.workflowNodesMap, @@ -85,7 +85,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode return node } - override exportJSON(): SerializedNode { + exportJSON(): SerializedNode { const json: SerializedNode = { type: 'workflow-variable-block', version: 1, @@ -119,7 +119,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode return self.__availableVariables } - override getTextContent(): string { + getTextContent(): string { return `{{#${this.getVariables().join('.')}#}}` } } diff --git a/web/app/components/base/prompt-log-modal/index.tsx b/web/app/components/base/prompt-log-modal/index.tsx index 08200623ae..6a79dfffeb 100644 --- a/web/app/components/base/prompt-log-modal/index.tsx +++ b/web/app/components/base/prompt-log-modal/index.tsx @@ -42,13 +42,13 @@ const PromptLogModal: FC = ({ }} ref={ref} > -
+
PROMPT LOG
{ currentLogItem.log?.length === 1 && ( <> - +
) diff --git a/web/app/components/base/select/locale-signin.tsx b/web/app/components/base/select/locale-signin.tsx index 046a76a5d4..3c5dd999f6 100644 --- a/web/app/components/base/select/locale-signin.tsx +++ b/web/app/components/base/select/locale-signin.tsx @@ -36,7 +36,7 @@ export default function LocaleSigninSelect({ leaveTo="transform opacity-0 scale-95" > -
+
{items.map((item) => { return ( diff --git a/web/app/components/base/spinner/index.tsx b/web/app/components/base/spinner/index.tsx index 48ee65b99f..65fea46a91 100644 --- a/web/app/components/base/spinner/index.tsx +++ b/web/app/components/base/spinner/index.tsx @@ -14,7 +14,7 @@ const Spinner: FC = ({ loading = false, children, className }) => { role="status" > Loading... diff --git a/web/app/components/base/tag-management/index.tsx b/web/app/components/base/tag-management/index.tsx index 6a050de834..e3748dfb33 100644 --- a/web/app/components/base/tag-management/index.tsx +++ b/web/app/components/base/tag-management/index.tsx @@ -48,8 +48,8 @@ const TagManagementModal = ({ show, type }: TagManagementModalProps) => { }, [type]) return ( setShowTagManagementModal(false)}> -
{t('tag.manageTags', { ns: 'common' })}
-
setShowTagManagementModal(false)}> +
{t('tag.manageTags', { ns: 'common' })}
+
setShowTagManagementModal(false)}>
diff --git a/web/app/components/base/tag-management/panel.tsx b/web/app/components/base/tag-management/panel.tsx index 3f64355c43..97fff5c816 100644 --- a/web/app/components/base/tag-management/panel.tsx +++ b/web/app/components/base/tag-management/panel.tsx @@ -114,7 +114,7 @@ const Panel = (props: PanelProps) => {
-
+
{`${t('tag.create', { ns: 'common' })} `} {`'${keywords}'`}
@@ -127,7 +127,7 @@ const Panel = (props: PanelProps) => { {filteredSelectedTagList.map(tag => (
selectTag(tag)} data-testid="tag-row"> -
+
{tag.name}
@@ -135,7 +135,7 @@ const Panel = (props: PanelProps) => { {filteredTagList.map(tag => (
selectTag(tag)} data-testid="tag-row"> -
+
{tag.name}
@@ -146,7 +146,7 @@ const Panel = (props: PanelProps) => {
-
{t('tag.noTag', { ns: 'common' })}
+
{t('tag.noTag', { ns: 'common' })}
)} @@ -154,7 +154,7 @@ const Panel = (props: PanelProps) => {
setShowTagManagementModal(true)}> -
+
{t('tag.manageTags', { ns: 'common' })}
diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx index cb83fdc366..da301a42c7 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx @@ -98,10 +98,10 @@ describe('CloudPlanItem', () => { />, ) - expect(screen.getByText('billing.plans.sandbox.name'))!.toBeInTheDocument() - expect(screen.getByText('billing.plans.sandbox.description'))!.toBeInTheDocument() - expect(screen.getByText('billing.plansCommon.free'))!.toBeInTheDocument() - expect(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' }))!.toBeInTheDocument() + expect(screen.getByText('billing.plans.sandbox.name')).toBeInTheDocument() + expect(screen.getByText('billing.plans.sandbox.description')).toBeInTheDocument() + expect(screen.getByText('billing.plansCommon.free')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' })).toBeInTheDocument() }) it('should display yearly pricing with discount when planRange is yearly', () => { @@ -115,9 +115,9 @@ describe('CloudPlanItem', () => { ) const professionalPlan = ALL_PLANS[Plan.professional] - expect(screen.getByText(`$${professionalPlan.price * 12}`))!.toBeInTheDocument() - expect(screen.getByText(`$${professionalPlan.price * 10}`))!.toBeInTheDocument() - expect(screen.getByText(/billing\.plansCommon\.priceTip.*billing\.plansCommon\.year/))!.toBeInTheDocument() + expect(screen.getByText(`$${professionalPlan.price * 12}`)).toBeInTheDocument() + expect(screen.getByText(`$${professionalPlan.price * 10}`)).toBeInTheDocument() + expect(screen.getByText(/billing\.plansCommon\.priceTip.*billing\.plansCommon\.year/)).toBeInTheDocument() }) it('should show "most popular" badge for professional plan', () => { @@ -130,7 +130,7 @@ describe('CloudPlanItem', () => { />, ) - expect(screen.getByText('billing.plansCommon.mostPopular'))!.toBeInTheDocument() + expect(screen.getByText('billing.plansCommon.mostPopular')).toBeInTheDocument() }) it('should not show "most popular" badge for non-professional plans', () => { @@ -157,7 +157,7 @@ describe('CloudPlanItem', () => { ) const button = screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' }) - expect(button)!.toBeDisabled() + expect(button).toBeDisabled() }) }) @@ -176,7 +176,7 @@ describe('CloudPlanItem', () => { ) fireEvent.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })) - expect(screen.getByText('billing.buyPermissionDeniedTip'))!.toBeInTheDocument() + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() expect(mockBillingInvoices).not.toHaveBeenCalled() }) @@ -320,7 +320,7 @@ describe('CloudPlanItem', () => { expect(openWindow).toHaveBeenCalledTimes(1) // The onError callback should have been passed to openAsyncWindow const callArgs = openWindow.mock.calls[0] - expect(callArgs![1]).toHaveProperty('onError') + expect(callArgs[1]).toHaveProperty('onError') }) }) @@ -336,39 +336,8 @@ describe('CloudPlanItem', () => { ) const teamPlan = ALL_PLANS[Plan.team] - expect(screen.getByText(`$${teamPlan.price}`))!.toBeInTheDocument() - expect(screen.getByText(/billing\.plansCommon\.priceTip.*billing\.plansCommon\.month/))!.toBeInTheDocument() - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price - // Should NOT show crossed-out yearly price + expect(screen.getByText(`$${teamPlan.price}`)).toBeInTheDocument() + expect(screen.getByText(/billing\.plansCommon\.priceTip.*billing\.plansCommon\.month/)).toBeInTheDocument() // Should NOT show crossed-out yearly price expect(screen.queryByText(`$${teamPlan.price * 12}`)).not.toBeInTheDocument() }) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index 53d5025f08..34d16bc2ff 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -103,38 +103,38 @@ const CloudPlanItem: FC = ({ {ICON_MAP[plan]}
-
{t(`${i18nPrefix}.name`, { ns: 'billing' })}
+
{t(`${i18nPrefix}.name`, { ns: 'billing' })}
{ isMostPopularPlan && (
- + {t('plansCommon.mostPopular', { ns: 'billing' })}
) }
-
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
+
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
{/* Price */} -
+
{isFreePlan && ( - {t('plansCommon.free', { ns: 'billing' })} + {t('plansCommon.free', { ns: 'billing' })} )} {!isFreePlan && ( <> {isYear && ( - + $ {planInfo.price * 12} )} - + $ {isYear ? planInfo.price * 10 : planInfo.price} - + {t('plansCommon.priceTip', { ns: 'billing' })} {t(`plansCommon.${!isYear ? 'month' : 'year'}`, { ns: 'billing' })} diff --git a/web/app/components/datasets/common/image-previewer/index.tsx b/web/app/components/datasets/common/image-previewer/index.tsx index 42b4ce33a9..fdc751cf7b 100644 --- a/web/app/components/datasets/common/image-previewer/index.tsx +++ b/web/app/components/datasets/common/image-previewer/index.tsx @@ -137,7 +137,7 @@ const ImagePreviewer = ({ return { ...prev, [image.url]: { - ...prev[image.url]!, + ...prev[image.url], status: 'loading', }, } @@ -168,15 +168,15 @@ const ImagePreviewer = ({ Esc
- {cachedImages[currentImage!.url]!.status === 'loading' && ( + {cachedImages[currentImage.url].status === 'loading' && ( )} - {cachedImages[currentImage!.url]!.status === 'error' && ( + {cachedImages[currentImage.url].status === 'error' && (
- {`Failed to load image: ${currentImage!.url}. Please try again.`} + {`Failed to load image: ${currentImage.url}. Please try again.`}
)} - {cachedImages[currentImage!.url]!.status === 'loaded' && ( + {cachedImages[currentImage.url].status === 'loaded' && (
{currentImage!.name}
- {currentImage!.name} + {currentImage.name} ยท - {`${cachedImages[currentImage!.url]!.width} ร—โ€Š ${cachedImages[currentImage!.url]!.height}`} + {`${cachedImages[currentImage.url].width} ร—โ€Š ${cachedImages[currentImage.url].height}`} ยท - {formatFileSize(currentImage!.size)} + {formatFileSize(currentImage.size)}
)} diff --git a/web/app/components/datasets/create/step-one/upgrade-card.tsx b/web/app/components/datasets/create/step-one/upgrade-card.tsx index e7016206ea..356e15ed43 100644 --- a/web/app/components/datasets/create/step-one/upgrade-card.tsx +++ b/web/app/components/datasets/create/step-one/upgrade-card.tsx @@ -15,9 +15,9 @@ const UpgradeCard: FC = () => { }, [setShowPricingModal]) return ( -
+
-
{t('upgrade.uploadMultipleFiles.title', { ns: 'billing' })}
+
{t('upgrade.uploadMultipleFiles.title', { ns: 'billing' })}
{t('upgrade.uploadMultipleFiles.description', { ns: 'billing' })}
{ render() // Should render Previous and Next buttons with correct text - // Should render Previous and Next buttons with correct text - expect(screen.getByText(/previousStep/i))!.toBeInTheDocument() - expect(screen.getByText(/nextStep/i))!.toBeInTheDocument() + expect(screen.getByText(/previousStep/i)).toBeInTheDocument() + expect(screen.getByText(/nextStep/i)).toBeInTheDocument() }) it('should render Previous and Next buttons when not in setting mode', () => { render() - expect(screen.getByText(/previousStep/i))!.toBeInTheDocument() - expect(screen.getByText(/nextStep/i))!.toBeInTheDocument() + expect(screen.getByText(/previousStep/i)).toBeInTheDocument() + expect(screen.getByText(/nextStep/i)).toBeInTheDocument() }) it('should render Save and Cancel buttons when in setting mode', () => { render() - expect(screen.getByText(/save/i))!.toBeInTheDocument() - expect(screen.getByText(/cancel/i))!.toBeInTheDocument() + expect(screen.getByText(/save/i)).toBeInTheDocument() + expect(screen.getByText(/cancel/i)).toBeInTheDocument() }) }) @@ -1773,14 +1772,14 @@ describe('StepTwoFooter', () => { render() const nextButton = screen.getByText(/nextStep/i).closest('button') - expect(nextButton)!.toBeDisabled() + expect(nextButton).toBeDisabled() }) it('should show loading state on Save button when creating in setting mode', () => { render() const saveButton = screen.getByText(/save/i).closest('button') - expect(saveButton)!.toBeDisabled() + expect(saveButton).toBeDisabled() }) }) }) @@ -1812,50 +1811,18 @@ describe('PreviewPanel', () => { render() // Check for the preview header title text - // Check for the preview header title text - expect(screen.getByText('datasetCreation.stepTwo.preview'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument() }) it('should render idle state when isIdle is true', () => { render() - expect(screen.getByText(/previewChunkTip/i))!.toBeInTheDocument() + expect(screen.getByText(/previewChunkTip/i)).toBeInTheDocument() }) it('should render loading skeleton when isPending is true', () => { render() - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers - // Should show skeleton containers // Should show skeleton containers expect(screen.queryByText(/previewChunkTip/i)).not.toBeInTheDocument() }) @@ -1874,7 +1841,7 @@ describe('PreviewPanel', () => { />, ) - expect(screen.getByText('Chunk 1 content'))!.toBeInTheDocument() + expect(screen.getByText('Chunk 1 content')).toBeInTheDocument() }) it('should render QA preview when docForm is qa', () => { @@ -1888,8 +1855,8 @@ describe('PreviewPanel', () => { />, ) - expect(screen.getByText('Q1'))!.toBeInTheDocument() - expect(screen.getByText('A1'))!.toBeInTheDocument() + expect(screen.getByText('Q1')).toBeInTheDocument() + expect(screen.getByText('A1')).toBeInTheDocument() }) it('should show chunk count badge for non-QA doc form', () => { @@ -1903,7 +1870,7 @@ describe('PreviewPanel', () => { />, ) - expect(screen.getByText(/25/))!.toBeInTheDocument() + expect(screen.getByText(/25/)).toBeInTheDocument() }) it('should render parent-child preview when docForm is parentChild', () => { @@ -1926,13 +1893,11 @@ describe('PreviewPanel', () => { ) // Should render parent chunk label - // Should render parent chunk label - expect(screen.getByText('Chunk-1'))!.toBeInTheDocument() + expect(screen.getByText('Chunk-1')).toBeInTheDocument() // Should render child chunks - // Should render child chunks - expect(screen.getByText('Child 1'))!.toBeInTheDocument() - expect(screen.getByText('Child 2'))!.toBeInTheDocument() - expect(screen.getByText('Child 3'))!.toBeInTheDocument() + expect(screen.getByText('Child 1')).toBeInTheDocument() + expect(screen.getByText('Child 2')).toBeInTheDocument() + expect(screen.getByText('Child 3')).toBeInTheDocument() }) it('should limit child chunks when chunkForContext is full-doc', () => { @@ -1955,43 +1920,10 @@ describe('PreviewPanel', () => { ) // Should render parent chunk - // Should render parent chunk - expect(screen.getByText('Chunk-1'))!.toBeInTheDocument() + expect(screen.getByText('Chunk-1')).toBeInTheDocument() // full-doc mode limits to FULL_DOC_PREVIEW_LENGTH (50) - // full-doc mode limits to FULL_DOC_PREVIEW_LENGTH (50) - expect(screen.getByText('ChildChunk1'))!.toBeInTheDocument() - expect(screen.getByText('ChildChunk50'))!.toBeInTheDocument() - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit - // Should not render beyond the limit + expect(screen.getByText('ChildChunk1')).toBeInTheDocument() + expect(screen.getByText('ChildChunk50')).toBeInTheDocument() // Should not render beyond the limit expect(screen.queryByText('ChildChunk51')).not.toBeInTheDocument() }) @@ -2012,10 +1944,10 @@ describe('PreviewPanel', () => { />, ) - expect(screen.getByText('Chunk-1'))!.toBeInTheDocument() - expect(screen.getByText('Chunk-2'))!.toBeInTheDocument() - expect(screen.getByText('P1-C1'))!.toBeInTheDocument() - expect(screen.getByText('P2-C1'))!.toBeInTheDocument() + expect(screen.getByText('Chunk-1')).toBeInTheDocument() + expect(screen.getByText('Chunk-2')).toBeInTheDocument() + expect(screen.getByText('P1-C1')).toBeInTheDocument() + expect(screen.getByText('P2-C1')).toBeInTheDocument() }) }) @@ -2290,20 +2222,19 @@ describe('StepTwo Component', () => { describe('Rendering', () => { it('should render without crashing', () => { render() - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) it('should show general chunking options when not in upload', () => { render() // Should render the segmentation section - // Should render the segmentation section - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) it('should show footer with Previous and Next buttons', () => { render() - expect(screen.getByText(/stepTwo\.previousStep/i))!.toBeInTheDocument() - expect(screen.getByText(/stepTwo\.nextStep/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.previousStep/i)).toBeInTheDocument() + expect(screen.getByText(/stepTwo\.nextStep/i)).toBeInTheDocument() }) }) @@ -2351,7 +2282,7 @@ describe('StepTwo Component', () => { render() // GeneralChunkingOptions renders a "Preview Chunk" button const previewButtons = screen.getAllByText(/stepTwo\.previewChunk/i) - fireEvent.click(previewButtons[0]!) + fireEvent.click(previewButtons[0]) // updatePreview calls estimateHook.fetchEstimate() // No error means the handler executed successfully }) @@ -2361,7 +2292,7 @@ describe('StepTwo Component', () => { // ParentChildOptions renders an OptionCard; find the title element and click its parent card const parentChildTitles = screen.getAllByText(/stepTwo\.parentChild/i) // The first match is the title; click it to trigger onDocFormChange - fireEvent.click(parentChildTitles[0]!) + fireEvent.click(parentChildTitles[0]) // handleDocFormChange sets docForm, segmentationType, and resets estimate }) }) @@ -2376,8 +2307,7 @@ describe('StepTwo Component', () => { />, ) // When currentDataset has parentChild doc_form, should show parent-child option - // When currentDataset has parentChild doc_form, should show parent-child option - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) it('should render setting mode with Save/Cancel buttons', () => { @@ -2390,8 +2320,8 @@ describe('StepTwo Component', () => { datasetId="test-id" />, ) - expect(screen.getByText(/stepTwo\.save/i))!.toBeInTheDocument() - expect(screen.getByText(/stepTwo\.cancel/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.save/i)).toBeInTheDocument() + expect(screen.getByText(/stepTwo\.cancel/i)).toBeInTheDocument() }) it('should call onCancel when Cancel button is clicked in setting mode', () => { @@ -2432,9 +2362,8 @@ describe('StepTwo Component', () => { it('should show both general and parent-child options in create page', () => { render() // When isInInit (no datasetId, no isSetting), both options should show - // When isInInit (no datasetId, no isSetting), both options should show - expect(screen.getByText('datasetCreation.stepTwo.general'))!.toBeInTheDocument() - expect(screen.getByText('datasetCreation.stepTwo.parentChild'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.general')).toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.parentChild')).toBeInTheDocument() }) it('should only show parent-child option when dataset has parentChild doc_form', () => { @@ -2447,9 +2376,7 @@ describe('StepTwo Component', () => { ) // showGeneralOption should be false (parentChild not in [text, qa]) // showParentChildOption should be true - // showGeneralOption should be false (parentChild not in [text, qa]) - // showParentChildOption should be true - expect(screen.getByText('datasetCreation.stepTwo.parentChild'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.parentChild')).toBeInTheDocument() }) it('should show general option only when dataset has text doc_form', () => { @@ -2461,8 +2388,7 @@ describe('StepTwo Component', () => { />, ) // showGeneralOption should be true (text is in [text, qa]) - // showGeneralOption should be true (text is in [text, qa]) - expect(screen.getByText('datasetCreation.stepTwo.general'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.general')).toBeInTheDocument() }) }) @@ -2475,7 +2401,7 @@ describe('StepTwo Component', () => { datasetId="test-id" />, ) - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) it('should show general option for empty dataset (no doc_form)', () => { @@ -2487,7 +2413,7 @@ describe('StepTwo Component', () => { datasetId="test-id" />, ) - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) it('should show both options in empty dataset upload', () => { @@ -2500,9 +2426,8 @@ describe('StepTwo Component', () => { />, ) // isUploadInEmptyDataset=true shows both options - // isUploadInEmptyDataset=true shows both options - expect(screen.getByText('datasetCreation.stepTwo.general'))!.toBeInTheDocument() - expect(screen.getByText('datasetCreation.stepTwo.parentChild'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.general')).toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.parentChild')).toBeInTheDocument() }) }) @@ -2510,22 +2435,19 @@ describe('StepTwo Component', () => { it('should render indexing mode section', () => { render() // IndexingModeSection renders the index mode title - // IndexingModeSection renders the index mode title - expect(screen.getByText(/stepTwo\.indexMode/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.indexMode/i)).toBeInTheDocument() }) it('should render embedding model selector when QUALIFIED', () => { render() // ModelSelector is mocked and rendered with data-testid - // ModelSelector is mocked and rendered with data-testid - expect(screen.getByTestId('model-selector'))!.toBeInTheDocument() + expect(screen.getByTestId('model-selector')).toBeInTheDocument() }) it('should render retrieval method config', () => { render() // RetrievalMethodConfig is mocked with data-testid - // RetrievalMethodConfig is mocked with data-testid - expect(screen.getByTestId('retrieval-method-config'))!.toBeInTheDocument() + expect(screen.getByTestId('retrieval-method-config')).toBeInTheDocument() }) it('should disable model and retrieval config when datasetId has existing data source', () => { @@ -2538,14 +2460,14 @@ describe('StepTwo Component', () => { ) // isModelAndRetrievalConfigDisabled should be true const modelSelector = screen.getByTestId('model-selector') - expect(modelSelector)!.toHaveAttribute('data-readonly', 'true') + expect(modelSelector).toHaveAttribute('data-readonly', 'true') }) }) describe('Preview Panel', () => { it('should render preview panel', () => { render() - expect(screen.getByText('datasetCreation.stepTwo.preview'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument() }) it('should hide document picker in setting mode', () => { @@ -2559,8 +2481,7 @@ describe('StepTwo Component', () => { />, ) // Preview panel should still render - // Preview panel should still render - expect(screen.getByText('datasetCreation.stepTwo.preview'))!.toBeInTheDocument() + expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument() }) }) @@ -2577,35 +2498,35 @@ describe('StepTwo Component', () => { it('should switch to QUALIFIED when selecting parentChild in ECONOMICAL mode', async () => { render() await vi.waitFor(() => { - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) const parentChildTitles = screen.getAllByText(/stepTwo\.parentChild/i) - fireEvent.click(parentChildTitles[0]!) + fireEvent.click(parentChildTitles[0]) }) it('should open QA confirm dialog and confirm switch when QA selected in ECONOMICAL mode', async () => { render() await vi.waitFor(() => { - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i) fireEvent.click(qaCheckbox) // Dialog should open โ†’ click Switch to confirm (triggers handleQAConfirm) const switchButton = await screen.findByText(/stepTwo\.switch/i) - expect(switchButton)!.toBeInTheDocument() + expect(switchButton).toBeInTheDocument() fireEvent.click(switchButton) }) it('should close QA confirm dialog when cancel is clicked', async () => { render() await vi.waitFor(() => { - expect(screen.getByText(/stepTwo\.segmentation/i))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument() }) // Open QA confirm dialog const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i) fireEvent.click(qaCheckbox) const dialogCancelButtons = await screen.findAllByText(/stepTwo\.cancel/i) - fireEvent.click(dialogCancelButtons[0]!) + fireEvent.click(dialogCancelButtons[0]) }) it('should handle picker change when selecting a different file', () => { @@ -2624,7 +2545,7 @@ describe('StepTwo Component', () => { render() // The default maxChunkLength (1024) now exceeds the limit (100) const previewButtons = screen.getAllByText(/stepTwo\.previewChunk/i) - fireEvent.click(previewButtons[0]!) + fireEvent.click(previewButtons[0]) // Restore document.body.removeAttribute('data-public-indexing-max-segmentation-tokens-length') }) diff --git a/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx b/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx index f1ab5392ce..2c0480e508 100644 --- a/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx +++ b/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx @@ -12,27 +12,26 @@ describe('DelimiterInput', () => { it('should render separator label', () => { render() - expect(screen.getByText(`${ns}.stepTwo.separator`))!.toBeInTheDocument() + expect(screen.getByText(`${ns}.stepTwo.separator`)).toBeInTheDocument() }) it('should render text input with placeholder', () => { render() const input = screen.getByPlaceholderText(`${ns}.stepTwo.separatorPlaceholder`) - expect(input)!.toBeInTheDocument() - expect(input)!.toHaveAttribute('type', 'text') + expect(input).toBeInTheDocument() + expect(input).toHaveAttribute('type', 'text') }) it('should pass through value and onChange props', () => { const onChange = vi.fn() render() - expect(screen.getByDisplayValue('test-val'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('test-val')).toBeInTheDocument() }) it('should render tooltip content', () => { render() // Tooltip triggers render; component mounts without error - // Tooltip triggers render; component mounts without error - expect(screen.getByText(`${ns}.stepTwo.separator`))!.toBeInTheDocument() + expect(screen.getByText(`${ns}.stepTwo.separator`)).toBeInTheDocument() }) it('should suppress onChange during IME composition', () => { @@ -48,7 +47,7 @@ describe('DelimiterInput', () => { fireEvent.compositionEnd(input) expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange.mock.calls[0]![0].target.value).toBe(finalValue) + expect(onChange.mock.calls[0][0].target.value).toBe(finalValue) }) }) @@ -59,24 +58,24 @@ describe('MaxLengthInput', () => { it('should render max length label', () => { render() - expect(screen.getByText(`${ns}.stepTwo.maxLength`))!.toBeInTheDocument() + expect(screen.getByText(`${ns}.stepTwo.maxLength`)).toBeInTheDocument() }) it('should render number input', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toBeInTheDocument() + expect(input).toBeInTheDocument() }) it('should accept value prop', () => { render() - expect(screen.getByRole('textbox'))!.toHaveValue('500') + expect(screen.getByRole('textbox')).toHaveValue('500') }) it('should have min of 1', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toBeInTheDocument() + expect(input).toBeInTheDocument() }) it('should reset to the minimum when users clear the value', () => { @@ -108,18 +107,18 @@ describe('OverlapInput', () => { it('should render number input', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toBeInTheDocument() + expect(input).toBeInTheDocument() }) it('should accept value prop', () => { render() - expect(screen.getByRole('textbox'))!.toHaveValue('50') + expect(screen.getByRole('textbox')).toHaveValue('50') }) it('should have min of 1', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toBeInTheDocument() + expect(input).toBeInTheDocument() }) it('should reset to the minimum when users clear the value', () => { diff --git a/web/app/components/datasets/create/website/firecrawl/__tests__/options.spec.tsx b/web/app/components/datasets/create/website/firecrawl/__tests__/options.spec.tsx index 946c04aa93..313ad9c051 100644 --- a/web/app/components/datasets/create/website/firecrawl/__tests__/options.spec.tsx +++ b/web/app/components/datasets/create/website/firecrawl/__tests__/options.spec.tsx @@ -35,10 +35,9 @@ describe('Options', () => { render() // Check that key elements are rendered - // Check that key elements are rendered - expect(screen.getByText(/crawlSubPage/i))!.toBeInTheDocument() - expect(screen.getByText(/limit/i))!.toBeInTheDocument() - expect(screen.getByText(/maxDepth/i))!.toBeInTheDocument() + expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument() + expect(screen.getByText(/limit/i)).toBeInTheDocument() + expect(screen.getByText(/maxDepth/i)).toBeInTheDocument() }) it('should render all form fields', () => { @@ -46,16 +45,14 @@ describe('Options', () => { render() // Checkboxes - // Checkboxes - expect(screen.getByText(/crawlSubPage/i))!.toBeInTheDocument() - expect(screen.getByText(/extractOnlyMainContent/i))!.toBeInTheDocument() + expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument() + expect(screen.getByText(/extractOnlyMainContent/i)).toBeInTheDocument() // Text/Number fields - // Text/Number fields - expect(screen.getByText(/limit/i))!.toBeInTheDocument() - expect(screen.getByText(/maxDepth/i))!.toBeInTheDocument() - expect(screen.getByText(/excludePaths/i))!.toBeInTheDocument() - expect(screen.getByText(/includeOnlyPaths/i))!.toBeInTheDocument() + expect(screen.getByText(/limit/i)).toBeInTheDocument() + expect(screen.getByText(/maxDepth/i)).toBeInTheDocument() + expect(screen.getByText(/excludePaths/i)).toBeInTheDocument() + expect(screen.getByText(/includeOnlyPaths/i)).toBeInTheDocument() }) it('should render with custom className', () => { @@ -65,7 +62,7 @@ describe('Options', () => { ) const rootElement = container.firstChild as HTMLElement - expect(rootElement)!.toHaveClass('custom-class') + expect(rootElement).toHaveClass('custom-class') }) it('should render limit field with required indicator', () => { @@ -74,7 +71,7 @@ describe('Options', () => { // Limit field should have required indicator (*) const requiredIndicator = screen.getByText('*') - expect(requiredIndicator)!.toBeInTheDocument() + expect(requiredIndicator).toBeInTheDocument() }) it('should render placeholder for excludes field', () => { @@ -82,7 +79,7 @@ describe('Options', () => { render() const excludesInput = screen.getByPlaceholderText('blog/*, /about/*') - expect(excludesInput)!.toBeInTheDocument() + expect(excludesInput).toBeInTheDocument() }) it('should render placeholder for includes field', () => { @@ -90,7 +87,7 @@ describe('Options', () => { render() const includesInput = screen.getByPlaceholderText('articles/*') - expect(includesInput)!.toBeInTheDocument() + expect(includesInput).toBeInTheDocument() }) it('should render two checkboxes', () => { @@ -109,8 +106,7 @@ describe('Options', () => { render() // First checkbox should have check icon when checked - // First checkbox should have check icon when checked - expect(screen.queryByTestId('check-icon-crawl-sub-page'))!.toBeInTheDocument() + expect(screen.queryByTestId('check-icon-crawl-sub-page')).toBeInTheDocument() }) it('should display crawl_sub_pages checkbox without check icon when false', () => { @@ -122,7 +118,7 @@ describe('Options', () => { it('should display only_main_content checkbox with check icon when true', () => { const payload = createMockCrawlOptions({ only_main_content: true }) render() - expect(screen.getByTestId('check-icon-only-main-content'))!.toBeInTheDocument() + expect(screen.getByTestId('check-icon-only-main-content')).toBeInTheDocument() }) it('should display only_main_content checkbox without check icon when false', () => { @@ -136,7 +132,7 @@ describe('Options', () => { render() const limitInput = screen.getByDisplayValue('25') - expect(limitInput)!.toBeInTheDocument() + expect(limitInput).toBeInTheDocument() }) it('should display max_depth value in input', () => { @@ -144,7 +140,7 @@ describe('Options', () => { render() const maxDepthInput = screen.getByDisplayValue('5') - expect(maxDepthInput)!.toBeInTheDocument() + expect(maxDepthInput).toBeInTheDocument() }) it('should display excludes value in input', () => { @@ -152,7 +148,7 @@ describe('Options', () => { render() const excludesInput = screen.getByDisplayValue('test/*') - expect(excludesInput)!.toBeInTheDocument() + expect(excludesInput).toBeInTheDocument() }) it('should display includes value in input', () => { @@ -160,7 +156,7 @@ describe('Options', () => { render() const includesInput = screen.getByDisplayValue('docs/*') - expect(includesInput)!.toBeInTheDocument() + expect(includesInput).toBeInTheDocument() }) }) @@ -170,7 +166,7 @@ describe('Options', () => { const { container } = render() const checkboxes = getCheckboxes(container) - fireEvent.click(checkboxes[0]!) + fireEvent.click(checkboxes[0]) expect(mockOnChange).toHaveBeenCalledWith({ ...payload, @@ -183,7 +179,7 @@ describe('Options', () => { const { container } = render() const checkboxes = getCheckboxes(container) - fireEvent.click(checkboxes[1]!) + fireEvent.click(checkboxes[1]) expect(mockOnChange).toHaveBeenCalledWith({ ...payload, @@ -255,8 +251,7 @@ describe('Options', () => { render() // Component should render without crashing - // Component should render without crashing - expect(screen.getByText(/limit/i))!.toBeInTheDocument() + expect(screen.getByText(/limit/i)).toBeInTheDocument() }) it('should handle zero values', () => { @@ -278,8 +273,8 @@ describe('Options', () => { }) render() - expect(screen.getByDisplayValue('9999'))!.toBeInTheDocument() - expect(screen.getByDisplayValue('100'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('9999')).toBeInTheDocument() + expect(screen.getByDisplayValue('100')).toBeInTheDocument() }) it('should handle special characters in text fields', () => { @@ -289,8 +284,8 @@ describe('Options', () => { }) render() - expect(screen.getByDisplayValue('path/*/file?query=1¶m=2'))!.toBeInTheDocument() - expect(screen.getByDisplayValue('docs/**/*.md'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('path/*/file?query=1¶m=2')).toBeInTheDocument() + expect(screen.getByDisplayValue('docs/**/*.md')).toBeInTheDocument() }) it('should preserve other payload fields when updating one field', () => { @@ -362,7 +357,7 @@ describe('Options', () => { rerender() - expect(screen.getByText(/limit/i))!.toBeInTheDocument() + expect(screen.getByText(/limit/i)).toBeInTheDocument() }) it('should re-render when payload changes', () => { @@ -370,10 +365,10 @@ describe('Options', () => { const payload2 = createMockCrawlOptions({ limit: 20 }) const { rerender } = render() - expect(screen.getByDisplayValue('10'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('10')).toBeInTheDocument() rerender() - expect(screen.getByDisplayValue('20'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('20')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/create/website/watercrawl/__tests__/options.spec.tsx b/web/app/components/datasets/create/website/watercrawl/__tests__/options.spec.tsx index b65df109ad..bda01dc152 100644 --- a/web/app/components/datasets/create/website/watercrawl/__tests__/options.spec.tsx +++ b/web/app/components/datasets/create/website/watercrawl/__tests__/options.spec.tsx @@ -34,12 +34,12 @@ describe('Options (watercrawl)', () => { const payload = createMockCrawlOptions() render() - expect(screen.getByText(/crawlSubPage/i))!.toBeInTheDocument() - expect(screen.getByText(/extractOnlyMainContent/i))!.toBeInTheDocument() - expect(screen.getByText(/limit/i))!.toBeInTheDocument() - expect(screen.getByText(/maxDepth/i))!.toBeInTheDocument() - expect(screen.getByText(/excludePaths/i))!.toBeInTheDocument() - expect(screen.getByText(/includeOnlyPaths/i))!.toBeInTheDocument() + expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument() + expect(screen.getByText(/extractOnlyMainContent/i)).toBeInTheDocument() + expect(screen.getByText(/limit/i)).toBeInTheDocument() + expect(screen.getByText(/maxDepth/i)).toBeInTheDocument() + expect(screen.getByText(/excludePaths/i)).toBeInTheDocument() + expect(screen.getByText(/includeOnlyPaths/i)).toBeInTheDocument() }) it('should render two checkboxes', () => { @@ -55,21 +55,21 @@ describe('Options (watercrawl)', () => { render() const requiredIndicator = screen.getByText('*') - expect(requiredIndicator)!.toBeInTheDocument() + expect(requiredIndicator).toBeInTheDocument() }) it('should render placeholder for excludes field', () => { const payload = createMockCrawlOptions() render() - expect(screen.getByPlaceholderText('blog/*, /about/*'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('blog/*, /about/*')).toBeInTheDocument() }) it('should render placeholder for includes field', () => { const payload = createMockCrawlOptions() render() - expect(screen.getByPlaceholderText('articles/*'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('articles/*')).toBeInTheDocument() }) it('should render with custom className', () => { @@ -79,7 +79,7 @@ describe('Options (watercrawl)', () => { ) const rootElement = container.firstChild as HTMLElement - expect(rootElement)!.toHaveClass('custom-class') + expect(rootElement).toHaveClass('custom-class') }) }) @@ -89,7 +89,7 @@ describe('Options (watercrawl)', () => { const payload = createMockCrawlOptions({ crawl_sub_pages: true }) render() - expect(screen.getByTestId('check-icon-crawl-sub-pages'))!.toBeInTheDocument() + expect(screen.getByTestId('check-icon-crawl-sub-pages')).toBeInTheDocument() }) it('should display crawl_sub_pages checkbox without check icon when false', () => { @@ -97,13 +97,13 @@ describe('Options (watercrawl)', () => { const { container } = render() const checkboxes = getCheckboxes(container) - expect(checkboxes[0]!.querySelector('svg')).not.toBeInTheDocument() + expect(checkboxes[0].querySelector('svg')).not.toBeInTheDocument() }) it('should display only_main_content checkbox with check icon when true', () => { const payload = createMockCrawlOptions({ only_main_content: true }) render() - expect(screen.getByTestId('check-icon-only-main-content'))!.toBeInTheDocument() + expect(screen.getByTestId('check-icon-only-main-content')).toBeInTheDocument() }) it('should display only_main_content checkbox without check icon when false', () => { @@ -111,35 +111,35 @@ describe('Options (watercrawl)', () => { const { container } = render() const checkboxes = getCheckboxes(container) - expect(checkboxes[1]!.querySelector('svg')).not.toBeInTheDocument() + expect(checkboxes[1].querySelector('svg')).not.toBeInTheDocument() }) it('should display limit value in input', () => { const payload = createMockCrawlOptions({ limit: 25 }) render() - expect(screen.getByDisplayValue('25'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('25')).toBeInTheDocument() }) it('should display max_depth value in input', () => { const payload = createMockCrawlOptions({ max_depth: 5 }) render() - expect(screen.getByDisplayValue('5'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('5')).toBeInTheDocument() }) it('should display excludes value in input', () => { const payload = createMockCrawlOptions({ excludes: 'test/*' }) render() - expect(screen.getByDisplayValue('test/*'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('test/*')).toBeInTheDocument() }) it('should display includes value in input', () => { const payload = createMockCrawlOptions({ includes: 'docs/*' }) render() - expect(screen.getByDisplayValue('docs/*'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('docs/*')).toBeInTheDocument() }) }) @@ -149,7 +149,7 @@ describe('Options (watercrawl)', () => { const { container } = render() const checkboxes = getCheckboxes(container) - fireEvent.click(checkboxes[0]!) + fireEvent.click(checkboxes[0]) expect(mockOnChange).toHaveBeenCalledWith({ ...payload, @@ -162,7 +162,7 @@ describe('Options (watercrawl)', () => { const { container } = render() const checkboxes = getCheckboxes(container) - fireEvent.click(checkboxes[1]!) + fireEvent.click(checkboxes[1]) expect(mockOnChange).toHaveBeenCalledWith({ ...payload, @@ -264,10 +264,10 @@ describe('Options (watercrawl)', () => { const payload2 = createMockCrawlOptions({ limit: 20 }) const { rerender } = render() - expect(screen.getByDisplayValue('10'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('10')).toBeInTheDocument() rerender() - expect(screen.getByDisplayValue('20'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('20')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx b/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx index 637e334166..049a5352fb 100644 --- a/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx @@ -105,7 +105,7 @@ describe('Operations', () => { describe('rendering', () => { it('should render without crashing', () => { render() - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) it('should render buttons when embeddingAvailable', () => { @@ -122,7 +122,7 @@ describe('Operations', () => { it('should render disabled switch when embeddingAvailable is false in list scene', () => { render() const disabledSwitch = screen.getByRole('switch') - expect(disabledSwitch)!.toHaveAttribute('aria-disabled', 'true') + expect(disabledSwitch).toHaveAttribute('aria-disabled', 'true') }) }) @@ -209,7 +209,7 @@ describe('Operations', () => { const buttons = screen.getAllByRole('button') const settingsButton = buttons[0] await act(async () => { - fireEvent.click(settingsButton!) + fireEvent.click(settingsButton) }) expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents/doc-1/settings') }) @@ -219,7 +219,7 @@ describe('Operations', () => { it('should render differently in detail scene', () => { render() const container = document.querySelector('.flex.items-center') - expect(container)!.toBeInTheDocument() + expect(container).toBeInTheDocument() }) it('should not render switch in detail scene', () => { @@ -239,7 +239,7 @@ describe('Operations', () => { onSelectedIdChange={mockOnSelectedIdChange} />, ) - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) }) @@ -257,8 +257,7 @@ describe('Operations', () => { render() await openPopover() // Check if popover content is visible - // Check if popover content is visible - expect(screen.getByText('datasetDocuments.list.table.rename'))!.toBeInTheDocument() + expect(screen.getByText('datasetDocuments.list.table.rename')).toBeInTheDocument() }) it('should call archive when archive action is clicked', async () => { @@ -298,8 +297,7 @@ describe('Operations', () => { fireEvent.click(deleteButton) }) // Check if confirmation modal is shown - // Check if confirmation modal is shown - expect(screen.getByText('datasetDocuments.list.delete.title'))!.toBeInTheDocument() + expect(screen.getByText('datasetDocuments.list.delete.title')).toBeInTheDocument() }) it('should call delete when confirm is clicked in delete modal', async () => { @@ -326,8 +324,7 @@ describe('Operations', () => { fireEvent.click(deleteButton) }) // Verify modal is shown - // Verify modal is shown - expect(screen.getByText('datasetDocuments.list.delete.title'))!.toBeInTheDocument() + expect(screen.getByText('datasetDocuments.list.delete.title')).toBeInTheDocument() // Find and click the cancel button const cancelButton = screen.getByText('common.operation.cancel') await act(async () => { @@ -369,7 +366,7 @@ describe('Operations', () => { await user.click(renameAction) const renameInput = await screen.findByRole('textbox') - expect(renameInput)!.toHaveValue('Test Document') + expect(renameInput).toHaveValue('Test Document') }) it('should call sync for notion data source', async () => { @@ -461,7 +458,7 @@ describe('Operations', () => { />, ) await openPopover() - expect(screen.getByText('datasetDocuments.list.action.download'))!.toBeInTheDocument() + expect(screen.getByText('datasetDocuments.list.action.download')).toBeInTheDocument() }) it('should download archived file when download is clicked', async () => { @@ -546,7 +543,7 @@ describe('Operations', () => { detail={{ ...defaultDetail, display_status: 'indexing' }} />, ) - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) it('should render resume action when status is paused', () => { @@ -556,7 +553,7 @@ describe('Operations', () => { detail={{ ...defaultDetail, display_status: 'paused' }} />, ) - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) it('should not show pause/resume for available status', async () => { @@ -585,7 +582,7 @@ describe('Operations', () => { detail={{ ...defaultDetail, data_source_type: 'notion_import' }} />, ) - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) it('should handle web data source type', () => { @@ -595,7 +592,7 @@ describe('Operations', () => { detail={{ ...defaultDetail, data_source_type: 'website_crawl' }} />, ) - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) it('should not show download for non-file data source', async () => { @@ -625,7 +622,7 @@ describe('Operations', () => { it('should accept custom className prop', () => { // The className is passed to CustomPopover, verify component renders without errors render() - expect(document.querySelector('.flex.items-center'))!.toBeInTheDocument() + expect(document.querySelector('.flex.items-center')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx b/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx index 01d6299492..97ae1c92a1 100644 --- a/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx @@ -112,26 +112,25 @@ describe('DocumentList', () => { describe('Rendering', () => { it('should render without crashing', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render all documents', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('Document 1.txt'))!.toBeInTheDocument() - expect(screen.getByText('Document 2.txt'))!.toBeInTheDocument() - expect(screen.getByText('Document 3.txt'))!.toBeInTheDocument() + expect(screen.getByText('Document 1.txt')).toBeInTheDocument() + expect(screen.getByText('Document 2.txt')).toBeInTheDocument() + expect(screen.getByText('Document 3.txt')).toBeInTheDocument() }) it('should render table headers', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('#'))!.toBeInTheDocument() + expect(screen.getByText('#')).toBeInTheDocument() }) it('should render pagination when total is provided', () => { render(, { wrapper: createWrapper() }) // Pagination component should be present - // Pagination component should be present - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should not render pagination when total is 0', () => { @@ -140,13 +139,13 @@ describe('DocumentList', () => { pagination: { ...defaultPagination, total: 0 }, } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render empty table when no documents', () => { const props = { ...defaultProps, documents: [] } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) }) @@ -166,8 +165,7 @@ describe('DocumentList', () => { const props = { ...defaultProps, embeddingAvailable: false } render(, { wrapper: createWrapper() }) // Row checkboxes should still be there, but header checkbox should be hidden - // Row checkboxes should still be there, but header checkbox should be hidden - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should call onSelectedIdChange when select all is clicked', () => { @@ -177,7 +175,7 @@ describe('DocumentList', () => { const checkboxes = findCheckboxes(container) if (checkboxes.length > 0) { - fireEvent.click(checkboxes[0]!) + fireEvent.click(checkboxes[0]) expect(onSelectedIdChange).toHaveBeenCalled() } }) @@ -192,7 +190,7 @@ describe('DocumentList', () => { // When checked, checkbox should have a check icon (svg) inside props.selectedIds.forEach((id) => { const checkIcon = screen.getByTestId(`check-icon-doc-row-${id}`) - expect(checkIcon)!.toBeInTheDocument() + expect(checkIcon).toBeInTheDocument() }) }) @@ -208,9 +206,7 @@ describe('DocumentList', () => { expect(checkboxes.length).toBeGreaterThan(0) // Header checkbox should show indeterminate icon, not check icon // Just verify it's rendered - // Header checkbox should show indeterminate icon, not check icon - // Just verify it's rendered - expect(checkboxes[0])!.toBeInTheDocument() + expect(checkboxes[0]).toBeInTheDocument() }) it('should call onSelectedIdChange with single document when row checkbox is clicked', () => { @@ -220,7 +216,7 @@ describe('DocumentList', () => { const checkboxes = findCheckboxes(container) if (checkboxes.length > 1) { - fireEvent.click(checkboxes[1]!) + fireEvent.click(checkboxes[1]) expect(onSelectedIdChange).toHaveBeenCalled() } }) @@ -240,7 +236,7 @@ describe('DocumentList', () => { const sortableHeaders = container.querySelectorAll('thead button') if (sortableHeaders.length > 0) - fireEvent.click(sortableHeaders[0]!) + fireEvent.click(sortableHeaders[0]) expect(onSortChange).toHaveBeenCalled() }) @@ -255,16 +251,14 @@ describe('DocumentList', () => { render(, { wrapper: createWrapper() }) // BatchAction component should be visible - // BatchAction component should be visible - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should not show batch action bar when no documents selected', () => { render(, { wrapper: createWrapper() }) // BatchAction should not be present - // BatchAction should not be present - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render batch action bar with archive option', () => { @@ -275,8 +269,7 @@ describe('DocumentList', () => { render(, { wrapper: createWrapper() }) // BatchAction component should be visible when documents are selected - // BatchAction component should be visible when documents are selected - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render batch action bar with enable option', () => { @@ -286,7 +279,7 @@ describe('DocumentList', () => { } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render batch action bar with disable option', () => { @@ -296,7 +289,7 @@ describe('DocumentList', () => { } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render batch action bar with delete option', () => { @@ -306,7 +299,7 @@ describe('DocumentList', () => { } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should clear selection when cancel is clicked', () => { @@ -336,8 +329,7 @@ describe('DocumentList', () => { render(, { wrapper: createWrapper() }) // BatchAction should be visible - // BatchAction should be visible - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should show re-index option for error documents', () => { @@ -351,8 +343,7 @@ describe('DocumentList', () => { render(, { wrapper: createWrapper() }) // BatchAction with re-index should be present for error documents - // BatchAction with re-index should be present for error documents - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) }) @@ -363,7 +354,7 @@ describe('DocumentList', () => { const rows = screen.getAllByRole('row') // First row is header, second row is first document if (rows.length > 1) { - fireEvent.click(rows[1]!) + fireEvent.click(rows[1]) expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents/doc-1') } }) @@ -385,11 +376,11 @@ describe('DocumentList', () => { const renameButtons = container.querySelectorAll('.cursor-pointer.rounded-md') if (renameButtons.length > 0) { await act(async () => { - fireEvent.click(renameButtons[0]!) + fireEvent.click(renameButtons[0]) }) } - expect(screen.getByRole('dialog', { name: 'datasetDocuments.list.table.rename' }))!.toBeInTheDocument() + expect(screen.getByRole('dialog', { name: 'datasetDocuments.list.table.rename' })).toBeInTheDocument() }) it('should call onUpdate when document is renamed', () => { @@ -398,8 +389,7 @@ describe('DocumentList', () => { render(, { wrapper: createWrapper() }) // The handleRenamed callback wraps onUpdate - // The handleRenamed callback wraps onUpdate - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) }) @@ -418,7 +408,7 @@ describe('DocumentList', () => { }) } - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should call onManageMetadata when manage metadata is triggered', () => { @@ -431,27 +421,26 @@ describe('DocumentList', () => { render(, { wrapper: createWrapper() }) // The onShowManage callback in EditMetadataBatchModal should call hideEditModal then onManageMetadata - // The onShowManage callback in EditMetadataBatchModal should call hideEditModal then onManageMetadata - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) }) describe('Chunking Mode', () => { it('should render with general mode', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render with QA mode', () => { // This test uses the default mock which returns ChunkingMode.text // The component will compute isQAMode based on doc_form render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should render with parent-child mode', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) }) @@ -460,7 +449,7 @@ describe('DocumentList', () => { const props = { ...defaultProps, documents: [] } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should handle documents with missing optional fields', () => { @@ -474,7 +463,7 @@ describe('DocumentList', () => { } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should handle remote sort value', () => { @@ -484,7 +473,7 @@ describe('DocumentList', () => { } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }) it('should handle large number of documents', () => { @@ -493,7 +482,7 @@ describe('DocumentList', () => { const props = { ...defaultProps, documents: manyDocs } render(, { wrapper: createWrapper() }) - expect(screen.getByRole('table'))!.toBeInTheDocument() + expect(screen.getByRole('table')).toBeInTheDocument() }, 10000) }) }) diff --git a/web/app/components/datasets/documents/components/document-list/components/__tests__/document-table-row.spec.tsx b/web/app/components/datasets/documents/components/document-list/components/__tests__/document-table-row.spec.tsx index b6b02ed829..d5e4f480be 100644 --- a/web/app/components/datasets/documents/components/document-list/components/__tests__/document-table-row.spec.tsx +++ b/web/app/components/datasets/documents/components/document-list/components/__tests__/document-table-row.spec.tsx @@ -103,23 +103,23 @@ describe('DocumentTableRow', () => { describe('Rendering', () => { it('should render without crashing', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('test-document.txt'))!.toBeInTheDocument() + expect(screen.getByText('test-document.txt')).toBeInTheDocument() }) it('should render index number correctly', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('6'))!.toBeInTheDocument() + expect(screen.getByText('6')).toBeInTheDocument() }) it('should render document name with tooltip', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('test-document.txt'))!.toBeInTheDocument() + expect(screen.getByText('test-document.txt')).toBeInTheDocument() }) it('should render checkbox element', () => { const { container } = render(, { wrapper: createWrapper() }) const checkbox = findCheckbox(container) - expect(checkbox)!.toBeInTheDocument() + expect(checkbox).toBeInTheDocument() }) }) @@ -127,14 +127,14 @@ describe('DocumentTableRow', () => { it('should show check icon when isSelected is true', () => { const { container } = render(, { wrapper: createWrapper() }) const checkbox = findCheckbox(container) - expect(checkbox)!.toBeInTheDocument() - expect(screen.getByTestId('check-icon-doc-row-doc-1'))!.toBeInTheDocument() + expect(checkbox).toBeInTheDocument() + expect(screen.getByTestId('check-icon-doc-row-doc-1')).toBeInTheDocument() }) it('should not show check icon when isSelected is false', () => { const { container } = render(, { wrapper: createWrapper() }) const checkbox = findCheckbox(container) - expect(checkbox)!.toBeInTheDocument() + expect(checkbox).toBeInTheDocument() expect(screen.queryByTestId('check-icon-doc-row-doc-1')).not.toBeInTheDocument() }) @@ -200,13 +200,13 @@ describe('DocumentTableRow', () => { it('should display word count less than 1000 as is', () => { const doc = createMockDoc({ word_count: 500 }) render(, { wrapper: createWrapper() }) - expect(screen.getByText('500'))!.toBeInTheDocument() + expect(screen.getByText('500')).toBeInTheDocument() }) it('should display word count 1000 or more in k format', () => { const doc = createMockDoc({ word_count: 1500 }) render(, { wrapper: createWrapper() }) - expect(screen.getByText('1.5k'))!.toBeInTheDocument() + expect(screen.getByText('1.5k')).toBeInTheDocument() }) it('should display 0 with empty style when word_count is 0', () => { @@ -219,7 +219,7 @@ describe('DocumentTableRow', () => { it('should handle undefined word_count', () => { const doc = createMockDoc({ word_count: undefined as unknown as number }) const { container } = render(, { wrapper: createWrapper() }) - expect(container)!.toBeInTheDocument() + expect(container).toBeInTheDocument() }) }) @@ -227,13 +227,13 @@ describe('DocumentTableRow', () => { it('should display hit count less than 1000 as is', () => { const doc = createMockDoc({ hit_count: 100 }) render(, { wrapper: createWrapper() }) - expect(screen.getByText('100'))!.toBeInTheDocument() + expect(screen.getByText('100')).toBeInTheDocument() }) it('should display hit count 1000 or more in k format', () => { const doc = createMockDoc({ hit_count: 2500 }) render(, { wrapper: createWrapper() }) - expect(screen.getByText('2.5k'))!.toBeInTheDocument() + expect(screen.getByText('2.5k')).toBeInTheDocument() }) it('should display 0 with empty style when hit_count is 0', () => { @@ -248,13 +248,12 @@ describe('DocumentTableRow', () => { it('should render ChunkingModeLabel with general mode', () => { render(, { wrapper: createWrapper() }) // ChunkingModeLabel should be rendered - // ChunkingModeLabel should be rendered - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) it('should render ChunkingModeLabel with QA mode', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) }) @@ -262,13 +261,13 @@ describe('DocumentTableRow', () => { it('should render SummaryStatus when summary_index_status is present', () => { const doc = createMockDoc({ summary_index_status: 'completed' }) render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) it('should not render SummaryStatus when summary_index_status is absent', () => { const doc = createMockDoc({ summary_index_status: undefined }) render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) }) @@ -283,7 +282,7 @@ describe('DocumentTableRow', () => { // Find the rename button by finding the RiEditLine icon's parent const renameButtons = container.querySelectorAll('.cursor-pointer.rounded-md') if (renameButtons.length > 0) { - fireEvent.click(renameButtons[0]!) + fireEvent.click(renameButtons[0]) expect(onShowRenameModal).toHaveBeenCalledWith(defaultProps.doc) expect(mockPush).not.toHaveBeenCalled() } @@ -293,13 +292,13 @@ describe('DocumentTableRow', () => { describe('Operations', () => { it('should pass selectedIds to Operations component', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) it('should pass onSelectedIdChange to Operations component', () => { const onSelectedIdChange = vi.fn() render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) }) @@ -307,7 +306,7 @@ describe('DocumentTableRow', () => { it('should render with FILE data source type', () => { const doc = createMockDoc({ data_source_type: DataSourceType.FILE }) render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) it('should render with NOTION data source type', () => { @@ -316,13 +315,13 @@ describe('DocumentTableRow', () => { data_source_info: { notion_page_icon: 'icon.png' }, }) render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) it('should render with WEB data source type', () => { const doc = createMockDoc({ data_source_type: DataSourceType.WEB }) render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) }) @@ -330,13 +329,13 @@ describe('DocumentTableRow', () => { it('should handle document with very long name', () => { const doc = createMockDoc({ name: `${'a'.repeat(500)}.txt` }) render(, { wrapper: createWrapper() }) - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) it('should handle document with special characters in name', () => { const doc = createMockDoc({ name: '.txt' }) render(, { wrapper: createWrapper() }) - expect(screen.getByText('.txt'))!.toBeInTheDocument() + expect(screen.getByText('.txt')).toBeInTheDocument() }) it('should memoize the component', () => { @@ -344,7 +343,7 @@ describe('DocumentTableRow', () => { const { rerender } = render(, { wrapper }) rerender() - expect(screen.getByRole('row'))!.toBeInTheDocument() + expect(screen.getByRole('row')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx index 46c6c6f462..2205bd5bb5 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx @@ -43,7 +43,7 @@ const Options = ({ if (!result.success) { const issues = result.error.issues const firstIssue = issues[0] - const errorMessage = `"${firstIssue!.path.join('.')}" ${firstIssue!.message}` + const errorMessage = `"${firstIssue.path.join('.')}" ${firstIssue.message}` toast.error(errorMessage) return errorMessage } diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx b/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx index 80cd737e7e..4a3f6ce5a2 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx @@ -33,7 +33,7 @@ const Form = ({ if (!result.success) { const issues = result.error.issues const firstIssue = issues[0] - const errorMessage = `"${firstIssue!.path.join('.')}" ${firstIssue!.message}` + const errorMessage = `"${firstIssue.path.join('.')}" ${firstIssue.message}` toast.error(errorMessage) return errorMessage } diff --git a/web/app/components/datasets/hit-testing/components/query-input/__tests__/index.spec.tsx b/web/app/components/datasets/hit-testing/components/query-input/__tests__/index.spec.tsx index d9427f5117..25b7abe7ea 100644 --- a/web/app/components/datasets/hit-testing/components/query-input/__tests__/index.spec.tsx +++ b/web/app/components/datasets/hit-testing/components/query-input/__tests__/index.spec.tsx @@ -79,17 +79,17 @@ describe('QueryInput', () => { it('should render title', () => { render() - expect(screen.getByText('datasetHitTesting.input.title'))!.toBeInTheDocument() + expect(screen.getByText('datasetHitTesting.input.title')).toBeInTheDocument() }) it('should render textarea with query text', () => { render() - expect(screen.getByTestId('textarea'))!.toBeInTheDocument() + expect(screen.getByTestId('textarea')).toBeInTheDocument() }) it('should render submit button', () => { render() - expect(screen.getByRole('button', { name: /input\.testing/ }))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: /input\.testing/ })).toBeInTheDocument() }) it('should disable submit button when text is empty', () => { @@ -98,17 +98,17 @@ describe('QueryInput', () => { queries: [{ content: '', content_type: 'text_query', file_info: null }] satisfies Query[], } render() - expect(screen.getByRole('button', { name: /input\.testing/ }))!.toBeDisabled() + expect(screen.getByRole('button', { name: /input\.testing/ })).toBeDisabled() }) it('should render retrieval method for non-external mode', () => { render() - expect(screen.getByText('dataset.retrieval.semantic_search.title'))!.toBeInTheDocument() + expect(screen.getByText('dataset.retrieval.semantic_search.title')).toBeInTheDocument() }) it('should render settings button for external mode', () => { render() - expect(screen.getByText('datasetHitTesting.settingTitle'))!.toBeInTheDocument() + expect(screen.getByText('datasetHitTesting.settingTitle')).toBeInTheDocument() }) it('should disable submit button when text exceeds 200 characters', () => { @@ -117,15 +117,15 @@ describe('QueryInput', () => { queries: [{ content: 'a'.repeat(201), content_type: 'text_query', file_info: null }] satisfies Query[], } render() - expect(screen.getByRole('button', { name: /input\.testing/ }))!.toBeDisabled() + expect(screen.getByRole('button', { name: /input\.testing/ })).toBeDisabled() }) it('should show loading state on submit button when loading', () => { render() const submitButton = screen.getByRole('button', { name: /input\.testing/ }) - expect(submitButton)!.toBeDisabled() - expect(submitButton)!.toHaveAttribute('aria-busy', 'true') - expect(submitButton.querySelector('.animate-spin'))!.toBeInTheDocument() + expect(submitButton).toBeDisabled() + expect(submitButton).toHaveAttribute('aria-busy', 'true') + expect(submitButton.querySelector('.animate-spin')).toBeInTheDocument() }) // Cover line 83: images useMemo with image_query data @@ -141,37 +141,6 @@ describe('QueryInput', () => { ] render() - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image - // Submit should be enabled since we have text + uploaded image // Submit should be enabled since we have text + uploaded image expect(screen.getByRole('button', { name: /input\.testing/ })).not.toBeDisabled() }) @@ -184,7 +153,7 @@ describe('QueryInput', () => { // Click settings button to open modal fireEvent.click(screen.getByRole('button', { name: /settingTitle/ })) - expect(screen.getByTestId('external-retrieval-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('external-retrieval-modal')).toBeInTheDocument() // Close modal fireEvent.click(screen.getByTestId('modal-close')) @@ -196,7 +165,7 @@ describe('QueryInput', () => { // Open modal fireEvent.click(screen.getByRole('button', { name: /settingTitle/ })) - expect(screen.getByTestId('external-retrieval-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('external-retrieval-modal')).toBeInTheDocument() // Save settings fireEvent.click(screen.getByTestId('modal-save')) @@ -305,7 +274,7 @@ describe('QueryInput', () => { ]), ) // Should not contain image_query - const calledWith = defaultProps.setQueries.mock.calls[0]![0] as Query[] + const calledWith = defaultProps.setQueries.mock.calls[0][0] as Query[] expect(calledWith.filter(q => q.content_type === 'image_query')).toHaveLength(0) }) }) @@ -443,7 +412,7 @@ describe('QueryInput', () => { it('should show keyword_search when isEconomy is true', () => { render() - expect(screen.getByText('dataset.retrieval.keyword_search.title'))!.toBeInTheDocument() + expect(screen.getByText('dataset.retrieval.keyword_search.title')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx index 060befe7d3..45ade1fffa 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx @@ -120,14 +120,14 @@ describe('EditMetadataBatchModal', () => { it('should render without crashing', async () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) }) it('should render document count', async () => { render() await waitFor(() => { - expect(screen.getByText(/5/))!.toBeInTheDocument() + expect(screen.getByText(/5/)).toBeInTheDocument() }) }) @@ -142,8 +142,8 @@ describe('EditMetadataBatchModal', () => { it('should render field names for existing items', async () => { render() await waitFor(() => { - expect(screen.getByText('field_one'))!.toBeInTheDocument() - expect(screen.getByText('field_two'))!.toBeInTheDocument() + expect(screen.getByText('field_one')).toBeInTheDocument() + expect(screen.getByText('field_two')).toBeInTheDocument() }) }) @@ -158,7 +158,7 @@ describe('EditMetadataBatchModal', () => { it('should render select metadata modal', async () => { render() await waitFor(() => { - expect(screen.getByTestId('select-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('select-modal')).toBeInTheDocument() }) }) }) @@ -169,7 +169,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) const cancelButton = screen.getByText(/cancel/i) @@ -183,7 +183,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // Find the primary save button (not the one in SelectMetadataModal) @@ -196,17 +196,17 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) const checkboxContainer = document.querySelector('[data-testid*="checkbox"]') - expect(checkboxContainer)!.toBeInTheDocument() + expect(checkboxContainer).toBeInTheDocument() if (checkboxContainer) { fireEvent.click(checkboxContainer) await waitFor(() => { const checkIcon = screen.getByTestId('check-icon-apply-to-all') - expect(checkIcon)!.toBeInTheDocument() + expect(checkIcon).toBeInTheDocument() }) } }) @@ -216,7 +216,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) }) }) @@ -226,7 +226,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('change-1')) @@ -239,7 +239,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('remove-1')) @@ -252,7 +252,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // First change the item @@ -269,14 +269,14 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('select-metadata')) // Should now have add-row for the new item await waitFor(() => { - expect(screen.getByTestId('add-row'))!.toBeInTheDocument() + expect(screen.getByTestId('add-row')).toBeInTheDocument() }) }) @@ -284,14 +284,14 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // First add an item fireEvent.click(screen.getByTestId('select-metadata')) await waitFor(() => { - expect(screen.getByTestId('add-row'))!.toBeInTheDocument() + expect(screen.getByTestId('add-row')).toBeInTheDocument() }) // Then remove it @@ -306,20 +306,20 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // First add an item fireEvent.click(screen.getByTestId('select-metadata')) await waitFor(() => { - expect(screen.getByTestId('add-row'))!.toBeInTheDocument() + expect(screen.getByTestId('add-row')).toBeInTheDocument() }) // Then change it fireEvent.click(screen.getByTestId('add-change-new-1')) - expect(screen.getByTestId('add-row'))!.toBeInTheDocument() + expect(screen.getByTestId('add-row')).toBeInTheDocument() }) it('should call doAddMetaData when saving new metadata with valid name', async () => { @@ -328,7 +328,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('save-metadata')) @@ -344,7 +344,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('save-metadata')) @@ -368,7 +368,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('save-metadata')) @@ -388,7 +388,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('manage-metadata')) @@ -401,14 +401,14 @@ describe('EditMetadataBatchModal', () => { it('should pass correct datasetId', async () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) }) it('should display correct document number', async () => { render() await waitFor(() => { - expect(screen.getByText(/10/))!.toBeInTheDocument() + expect(screen.getByText(/10/)).toBeInTheDocument() }) }) @@ -427,7 +427,7 @@ describe('EditMetadataBatchModal', () => { ] render() await waitFor(() => { - expect(screen.getByTestId('edit-row'))!.toBeInTheDocument() + expect(screen.getByTestId('edit-row')).toBeInTheDocument() }) }) @@ -436,7 +436,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // Find the primary save button @@ -453,7 +453,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) @@ -470,7 +470,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) const checkboxContainer = document.querySelector('[data-testid*="checkbox"]') @@ -493,7 +493,7 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // Remove an item @@ -503,7 +503,7 @@ describe('EditMetadataBatchModal', () => { expect(onSave).toHaveBeenCalled() // The first argument should not contain the deleted item (id '1') - const savedList = onSave.mock.calls[0]![0] as MetadataItemInBatchEdit[] + const savedList = onSave.mock.calls[0][0] as MetadataItemInBatchEdit[] const hasDeletedItem = savedList.some(item => item.id === '1') expect(hasDeletedItem).toBe(false) }) @@ -512,13 +512,13 @@ describe('EditMetadataBatchModal', () => { render() await waitFor(() => { - expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() }) // Add first item fireEvent.click(screen.getByTestId('select-metadata')) await waitFor(() => { - expect(screen.getByTestId('add-row'))!.toBeInTheDocument() + expect(screen.getByTestId('add-row')).toBeInTheDocument() }) // Remove it @@ -531,7 +531,7 @@ describe('EditMetadataBatchModal', () => { // Add again fireEvent.click(screen.getByTestId('select-metadata')) await waitFor(() => { - expect(screen.getByTestId('add-row'))!.toBeInTheDocument() + expect(screen.getByTestId('add-row')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx b/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx index 71f23324f6..ddd624a076 100644 --- a/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx @@ -99,7 +99,7 @@ describe('MetadataDocument', () => { docDetail={mockDocDetail as Parameters[0]['docDetail']} />, ) - expect(container.firstChild)!.toBeInTheDocument() + expect(container.firstChild).toBeInTheDocument() }) it('should render metadata fields when hasData is true', () => { @@ -110,8 +110,8 @@ describe('MetadataDocument', () => { docDetail={mockDocDetail as Parameters[0]['docDetail']} />, ) - expect(screen.getByText('field_one'))!.toBeInTheDocument() - expect(screen.getByText('field_two'))!.toBeInTheDocument() + expect(screen.getByText('field_one')).toBeInTheDocument() + expect(screen.getByText('field_two')).toBeInTheDocument() }) it('should render no-data state when hasData is false and not in edit mode', () => { @@ -147,8 +147,8 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText(/save/i))!.toBeInTheDocument() - expect(screen.getByText(/cancel/i))!.toBeInTheDocument() + expect(screen.getByText(/save/i)).toBeInTheDocument() + expect(screen.getByText(/cancel/i)).toBeInTheDocument() }) it('should render built-in section when builtInEnabled is true', () => { @@ -166,7 +166,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('created_at'))!.toBeInTheDocument() + expect(screen.getByText('created_at')).toBeInTheDocument() }) it('should render divider when builtInEnabled is true', () => { @@ -185,7 +185,7 @@ describe('MetadataDocument', () => { ) const divider = container.querySelector('.bg-linear-to-r') - expect(divider)!.toBeInTheDocument() + expect(divider).toBeInTheDocument() }) it('should render origin info section', () => { @@ -202,7 +202,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('source'))!.toBeInTheDocument() + expect(screen.getByText('source')).toBeInTheDocument() }) it('should render technical parameters section', () => { @@ -219,7 +219,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('word_count'))!.toBeInTheDocument() + expect(screen.getByText('word_count')).toBeInTheDocument() }) it('should render all sections together', () => { @@ -239,10 +239,10 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('field_one'))!.toBeInTheDocument() - expect(screen.getByText('created_at'))!.toBeInTheDocument() - expect(screen.getByText('source'))!.toBeInTheDocument() - expect(screen.getByText('word_count'))!.toBeInTheDocument() + expect(screen.getByText('field_one')).toBeInTheDocument() + expect(screen.getByText('created_at')).toBeInTheDocument() + expect(screen.getByText('source')).toBeInTheDocument() + expect(screen.getByText('word_count')).toBeInTheDocument() }) }) @@ -255,7 +255,7 @@ describe('MetadataDocument', () => { docDetail={mockDocDetail as Parameters[0]['docDetail']} />, ) - expect(screen.getByText(/edit/i))!.toBeInTheDocument() + expect(screen.getByText(/edit/i)).toBeInTheDocument() }) it('should call startToEdit when edit button is clicked', () => { @@ -362,9 +362,8 @@ describe('MetadataDocument', () => { ) // Should show save/cancel buttons - // Should show save/cancel buttons - expect(screen.getByText(/save/i))!.toBeInTheDocument() - expect(screen.getByText(/cancel/i))!.toBeInTheDocument() + expect(screen.getByText(/save/i)).toBeInTheDocument() + expect(screen.getByText(/cancel/i)).toBeInTheDocument() }) }) @@ -387,7 +386,7 @@ describe('MetadataDocument', () => { const inputs = container.querySelectorAll('input') if (inputs.length > 0) { - fireEvent.change(inputs[0]!, { target: { value: 'new value' } }) + fireEvent.change(inputs[0], { target: { value: 'new value' } }) await waitFor(() => { expect(setTempList).toHaveBeenCalled() @@ -455,7 +454,7 @@ describe('MetadataDocument', () => { const inputs = container.querySelectorAll('input') if (inputs.length > 0) { - fireEvent.change(inputs[0]!, { target: { value: 'updated' } }) + fireEvent.change(inputs[0], { target: { value: 'updated' } }) await waitFor(() => { expect(setTempList).toHaveBeenCalled() }) @@ -484,7 +483,7 @@ describe('MetadataDocument', () => { expect(deleteContainers.length).toBeGreaterThan(0) if (deleteContainers.length > 0) { - const deleteIcon = deleteContainers[0]!.querySelector('svg') + const deleteIcon = deleteContainers[0].querySelector('svg') if (deleteIcon) fireEvent.click(deleteIcon) @@ -505,7 +504,7 @@ describe('MetadataDocument', () => { className="custom-class" />, ) - expect(container.firstChild)!.toHaveClass('custom-class') + expect(container.firstChild).toHaveClass('custom-class') }) it('should use tempList when in edit mode', () => { @@ -525,7 +524,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('temp_field'))!.toBeInTheDocument() + expect(screen.getByText('temp_field')).toBeInTheDocument() }) it('should use list when not in edit mode', () => { @@ -537,8 +536,8 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('field_one'))!.toBeInTheDocument() - expect(screen.getByText('field_two'))!.toBeInTheDocument() + expect(screen.getByText('field_one')).toBeInTheDocument() + expect(screen.getByText('field_two')).toBeInTheDocument() }) it('should pass datasetId to child components', () => { @@ -550,8 +549,7 @@ describe('MetadataDocument', () => { />, ) // Component should render without errors - // Component should render without errors - expect(screen.getByText('field_one'))!.toBeInTheDocument() + expect(screen.getByText('field_one')).toBeInTheDocument() }) }) @@ -590,37 +588,6 @@ describe('MetadataDocument', () => { />, ) - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered - // NoData component should not be rendered // NoData component should not be rendered expect(screen.queryByText(/start/i)).not.toBeInTheDocument() }) @@ -640,37 +607,6 @@ describe('MetadataDocument', () => { />, ) - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined - // headerRight should be null/undefined // headerRight should be null/undefined expect(screen.queryByText(/^edit$/i)).not.toBeInTheDocument() }) @@ -692,7 +628,7 @@ describe('MetadataDocument', () => { docDetail={mockDocDetail as Parameters[0]['docDetail']} />, ) - expect(container.firstChild)!.toBeInTheDocument() + expect(container.firstChild).toBeInTheDocument() }) it('should render correctly with minimal props', () => { @@ -703,7 +639,7 @@ describe('MetadataDocument', () => { docDetail={mockDocDetail as Parameters[0]['docDetail']} />, ) - expect(container.firstChild)!.toBeInTheDocument() + expect(container.firstChild).toBeInTheDocument() }) it('should handle switching between view and edit mode', () => { @@ -715,7 +651,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText(/edit/i))!.toBeInTheDocument() + expect(screen.getByText(/edit/i)).toBeInTheDocument() unmount() @@ -732,8 +668,8 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText(/save/i))!.toBeInTheDocument() - expect(screen.getByText(/cancel/i))!.toBeInTheDocument() + expect(screen.getByText(/save/i)).toBeInTheDocument() + expect(screen.getByText(/cancel/i)).toBeInTheDocument() }) it('should handle multiple items in all sections', () => { @@ -766,11 +702,11 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('user_field_1'))!.toBeInTheDocument() - expect(screen.getByText('user_field_2'))!.toBeInTheDocument() - expect(screen.getByText('created_at'))!.toBeInTheDocument() - expect(screen.getByText('source'))!.toBeInTheDocument() - expect(screen.getByText('word_count'))!.toBeInTheDocument() + expect(screen.getByText('user_field_1')).toBeInTheDocument() + expect(screen.getByText('user_field_2')).toBeInTheDocument() + expect(screen.getByText('created_at')).toBeInTheDocument() + expect(screen.getByText('source')).toBeInTheDocument() + expect(screen.getByText('word_count')).toBeInTheDocument() }) it('should handle null values in metadata', () => { @@ -789,7 +725,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('null_field'))!.toBeInTheDocument() + expect(screen.getByText('null_field')).toBeInTheDocument() }) it('should handle undefined values in metadata', () => { @@ -808,7 +744,7 @@ describe('MetadataDocument', () => { />, ) - expect(screen.getByText('undefined_field'))!.toBeInTheDocument() + expect(screen.getByText('undefined_field')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx b/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx index 5e81611fc4..7441274155 100644 --- a/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx @@ -19,12 +19,12 @@ describe('IndexMethod', () => { describe('Rendering', () => { it('should render without crashing', () => { render() - expect(screen.getByText(/stepTwo\.qualified/))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.qualified/)).toBeInTheDocument() }) it('should render High Quality option', () => { render() - expect(screen.getByText(/stepTwo\.qualified/))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.qualified/)).toBeInTheDocument() }) it('should render Economy option', () => { @@ -34,17 +34,17 @@ describe('IndexMethod', () => { it('should render High Quality description', () => { render() - expect(screen.getByText(/form\.indexMethodHighQualityTip/))!.toBeInTheDocument() + expect(screen.getByText(/form\.indexMethodHighQualityTip/)).toBeInTheDocument() }) it('should render Economy description', () => { render() - expect(screen.getByText(/form\.indexMethodEconomyTip/))!.toBeInTheDocument() + expect(screen.getByText(/form\.indexMethodEconomyTip/)).toBeInTheDocument() }) it('should render recommended badge on High Quality', () => { render() - expect(screen.getByText(/stepTwo\.recommend/))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.recommend/)).toBeInTheDocument() }) }) @@ -82,7 +82,7 @@ describe('IndexMethod', () => { // Find and click Economy option - use getAllByText and get the first one (title) const economyTitles = screen.getAllByText(/form\.indexMethodEconomy/) const economyTitle = economyTitles[0] - const card = economyTitle!.closest('div')?.parentElement?.parentElement?.parentElement + const card = economyTitle.closest('div')?.parentElement?.parentElement?.parentElement fireEvent.click(card!) expect(handleChange).toHaveBeenCalledWith(IndexingType.ECONOMICAL) @@ -114,7 +114,7 @@ describe('IndexMethod', () => { // Try to click Economy option - use getAllByText and get the first one (title) const economyTitles = screen.getAllByText(/form\.indexMethodEconomy/) const economyTitle = economyTitles[0] - const card = economyTitle!.closest('div')?.parentElement?.parentElement?.parentElement + const card = economyTitle.closest('div')?.parentElement?.parentElement?.parentElement fireEvent.click(card!) // Should not call onChange because Economy is disabled when current is QUALIFIED @@ -125,13 +125,13 @@ describe('IndexMethod', () => { describe('KeywordNumber', () => { it('should render KeywordNumber component inside Economy option', () => { render() - expect(getKeywordSlider())!.toBeInTheDocument() + expect(getKeywordSlider()).toBeInTheDocument() }) it('should pass keywordNumber to KeywordNumber component', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toHaveValue('25') + expect(input).toHaveValue('25') }) it('should call onKeywordNumberChange when KeywordNumber changes', () => { @@ -160,13 +160,13 @@ describe('IndexMethod', () => { it('should show orange effect color for High Quality option', () => { const { container } = render() const orangeEffect = container.querySelector('.bg-util-colors-orange-orange-500') - expect(orangeEffect)!.toBeInTheDocument() + expect(orangeEffect).toBeInTheDocument() }) it('should show indigo effect color for Economy option', () => { const { container } = render() const indigoEffect = container.querySelector('.bg-util-colors-indigo-indigo-600') - expect(indigoEffect)!.toBeInTheDocument() + expect(indigoEffect).toBeInTheDocument() }) }) @@ -188,20 +188,19 @@ describe('IndexMethod', () => { it('should handle undefined currentValue', () => { render() // Should render without error - // Should render without error - expect(screen.getByText(/stepTwo\.qualified/))!.toBeInTheDocument() + expect(screen.getByText(/stepTwo\.qualified/)).toBeInTheDocument() }) it('should handle minimum keywordNumber', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toHaveValue('0') + expect(input).toHaveValue('0') }) it('should handle max keywordNumber', () => { render() const input = screen.getByRole('textbox') - expect(input)!.toHaveValue('50') + expect(input).toHaveValue('50') }) }) }) diff --git a/web/app/components/datasets/settings/index-method/keyword-number.tsx b/web/app/components/datasets/settings/index-method/keyword-number.tsx index 1f467e1798..6f126265e1 100644 --- a/web/app/components/datasets/settings/index-method/keyword-number.tsx +++ b/web/app/components/datasets/settings/index-method/keyword-number.tsx @@ -33,7 +33,7 @@ const KeyWordNumber = ({ return (
-
+
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
- - + + - - + + diff --git a/web/app/components/datasets/settings/permission-selector/index.tsx b/web/app/components/datasets/settings/permission-selector/index.tsx index a9b0c348d8..7452b60104 100644 --- a/web/app/components/datasets/settings/permission-selector/index.tsx +++ b/web/app/components/datasets/settings/permission-selector/index.tsx @@ -133,8 +133,8 @@ const PermissionSelector = ({ { selectedMembers.length === 1 && ( ) @@ -143,14 +143,14 @@ const PermissionSelector = ({ selectedMembers.length >= 2 && ( <> diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index f356bd71b2..4b8a5e2d8a 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -130,7 +130,7 @@ export default function AccountSetting({ ], }, ] - const activeItem = [...menuItems[0]!.items, ...menuItems[1]!.items].find(item => item.key === activeMenu) + const activeItem = [...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu) const [searchValue, setSearchValue] = useState('') diff --git a/web/app/components/header/account-setting/key-validator/KeyInput.tsx b/web/app/components/header/account-setting/key-validator/KeyInput.tsx index c352cc4835..cf8f88be7e 100644 --- a/web/app/components/header/account-setting/key-validator/KeyInput.tsx +++ b/web/app/components/header/account-setting/key-validator/KeyInput.tsx @@ -60,8 +60,8 @@ const KeyInput = ({ { return ( <>
-
+
- {currentWorkspace?.name[0]?.toLocaleUpperCase()} + {currentWorkspace?.name[0]?.toLocaleUpperCase()}
-
+
{currentWorkspace?.name} {isCurrentWorkspaceOwner && ( @@ -82,7 +82,7 @@ const MembersPage = () => { )}
-
+
{enableBilling && isNotUnlimitedMemberPlan ? (
@@ -116,9 +116,9 @@ const MembersPage = () => {
-
{t('members.name', { ns: 'common' })}
-
{t('members.lastActive', { ns: 'common' })}
-
{t('members.role', { ns: 'common' })}
+
{t('members.name', { ns: 'common' })}
+
{t('members.lastActive', { ns: 'common' })}
+
{t('members.role', { ns: 'common' })}
{ @@ -127,27 +127,27 @@ const MembersPage = () => {
-
+
{account.name} - {account.status === 'pending' && {t('members.pending', { ns: 'common' })}} - {userProfile.email === account.email && {t('members.you', { ns: 'common' })}} + {account.status === 'pending' && {t('members.pending', { ns: 'common' })}} + {userProfile.email === account.email && {t('members.you', { ns: 'common' })}}
-
{account.email}
+
{account.email}
-
{formatTimeFromNow(Number((account.last_active_at || account.created_at)) * 1000)}
+
{formatTimeFromNow(Number((account.last_active_at || account.created_at)) * 1000)}
{isCurrentWorkspaceOwner && account.role === 'owner' && isAllowTransferWorkspace && ( setShowTransferOwnershipModal(true)}> )} {isCurrentWorkspaceOwner && account.role === 'owner' && !isAllowTransferWorkspace && ( -
{RoleMap[account.role] || RoleMap.normal}
+
{RoleMap[account.role] || RoleMap.normal}
)} {isCurrentWorkspaceOwner && account.role !== 'owner' && ( )} {!isCurrentWorkspaceOwner && ( -
{RoleMap[account.role] || RoleMap.normal}
+
{RoleMap[account.role] || RoleMap.normal}
)}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx index be3e9edb93..0f507a24c6 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx @@ -58,9 +58,9 @@ const CredentialSelector = ({ selectedCredential && (
{ - !selectedCredential.addNewCredential && + !selectedCredential.addNewCredential && } -
{selectedCredential.credential_name}
+
{selectedCredential.credential_name}
{ selectedCredential.from_enterprise && ( Enterprise @@ -71,7 +71,7 @@ const CredentialSelector = ({ } { !selectedCredential && ( -
{t('modelProvider.auth.selectModelCredential', { ns: 'common' })}
+
{t('modelProvider.auth.selectModelCredential', { ns: 'common' })}
) } @@ -98,7 +98,7 @@ const CredentialSelector = ({ { !notAllowAddNewCredential && (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx index cca5846390..3cb37fd4e5 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx @@ -20,12 +20,12 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa
e.stopPropagation()}>
{title}
{description && ( -
+
{description}
)} {linkText && linkHref && ( -
+
{ diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx index 34b9d1578f..b3a0ec225b 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx @@ -164,7 +164,7 @@ const ModelLoadBalancingModal = ({ provider, configurateMethod, currentCustomCon const prevIndex = newConfigs.findIndex(item => item.credential_id === modelCredential.credential_id && item.name !== '__inherit__') const newIndex = available_credentials.findIndex(c => c.credential_id === modelCredential.credential_id) if (newIndex > -1 && prevIndex > -1) - newConfigs[prevIndex]!.name = available_credentials[newIndex]!.credential_name || '' + newConfigs[prevIndex].name = available_credentials[newIndex].credential_name || '' return { ...prev, configs: newConfigs, diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index d92fd032fd..560d5f1eaa 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -45,7 +45,7 @@ const Header = () => { const renderLogo = () => (

- + {isBrandingEnabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'} {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo ? ( @@ -91,7 +91,7 @@ const Header = () => { return (
-
+
{renderLogo()}
/
@@ -105,7 +105,7 @@ const Header = () => { {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } {!isCurrentWorkspaceDatasetOperator && }
-
+
diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index c91110b8cc..c3c1ab5fb1 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -9,10 +9,10 @@ const Description = () => { return ( <> -

+

{t('marketplace.empower')}

-

+

{ isZhHans && ( <> @@ -29,31 +29,31 @@ const Description = () => { ) } - + {t('category.models')} , - + {t('category.tools')} , - + {t('category.datasources')} , - + {t('category.triggers')} , - + {t('category.agents')} , - + {t('category.extensions')} {t('marketplace.and')} - + {t('category.bundles')} { diff --git a/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx b/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx index 8609ba5539..31cb5d8445 100644 --- a/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx +++ b/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx @@ -124,7 +124,7 @@ describe('SearchBox', () => { it('should render without crashing', () => { render() - expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should render with marketplace mode styling', () => { @@ -133,8 +133,7 @@ describe('SearchBox', () => { ) // In marketplace mode, TagsFilter comes before input - // In marketplace mode, TagsFilter comes before input - expect(container.querySelector('.rounded-xl'))!.toBeInTheDocument() + expect(container.querySelector('.rounded-xl')).toBeInTheDocument() }) it('should render with non-marketplace mode styling', () => { @@ -143,26 +142,25 @@ describe('SearchBox', () => { ) // In non-marketplace mode, search icon appears first - // In non-marketplace mode, search icon appears first - expect(container.querySelector('.rounded-lg'))!.toBeInTheDocument() + expect(container.querySelector('.rounded-lg')).toBeInTheDocument() }) it('should render placeholder correctly', () => { render() - expect(screen.getByPlaceholderText('Search here...'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('Search here...')).toBeInTheDocument() }) it('should render search input with current value', () => { render() - expect(screen.getByDisplayValue('test query'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('test query')).toBeInTheDocument() }) it('should render TagsFilter component', () => { render() - expect(screen.getByTestId('portal-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-elem')).toBeInTheDocument() }) }) @@ -177,9 +175,8 @@ describe('SearchBox', () => { const input = screen.getByRole('textbox') // Both should be rendered - // Both should be rendered - expect(portalElem)!.toBeInTheDocument() - expect(input)!.toBeInTheDocument() + expect(portalElem).toBeInTheDocument() + expect(input).toBeInTheDocument() }) it('should render clear button when search has value in marketplace mode', () => { @@ -210,8 +207,7 @@ describe('SearchBox', () => { ) // Search icon should be present - // Search icon should be present - expect(container.querySelector('.text-components-input-text-placeholder'))!.toBeInTheDocument() + expect(container.querySelector('.text-components-input-text-placeholder')).toBeInTheDocument() }) it('should render clear button when search has value', () => { @@ -227,8 +223,8 @@ describe('SearchBox', () => { const portalElem = screen.getByTestId('portal-elem') const input = screen.getByRole('textbox') - expect(portalElem)!.toBeInTheDocument() - expect(input)!.toBeInTheDocument() + expect(portalElem).toBeInTheDocument() + expect(input).toBeInTheDocument() }) it('should set autoFocus when prop is true', () => { @@ -236,8 +232,7 @@ describe('SearchBox', () => { const input = screen.getByRole('textbox') // autoFocus is a boolean attribute that React handles specially - // autoFocus is a boolean attribute that React handles specially - expect(input)!.toBeInTheDocument() + expect(input).toBeInTheDocument() }) }) @@ -269,7 +264,7 @@ describe('SearchBox', () => { const buttons = screen.getAllByRole('button') // Find the clear button (the one in the search area) const clearButton = buttons[buttons.length - 1] - fireEvent.click(clearButton!) + fireEvent.click(clearButton) expect(onSearchChange).toHaveBeenCalledWith('') }) @@ -287,7 +282,7 @@ describe('SearchBox', () => { const buttons = screen.getAllByRole('button') // First button should be the clear button in non-marketplace mode - fireEvent.click(buttons[0]!) + fireEvent.click(buttons[0]) expect(onSearchChange).toHaveBeenCalledWith('') }) @@ -361,7 +356,7 @@ describe('SearchBox', () => { , ) - expect(container.querySelector('.custom-wrapper-class'))!.toBeInTheDocument() + expect(container.querySelector('.custom-wrapper-class')).toBeInTheDocument() }) it('should apply inputClassName correctly', () => { @@ -369,19 +364,19 @@ describe('SearchBox', () => { , ) - expect(container.querySelector('.custom-input-class'))!.toBeInTheDocument() + expect(container.querySelector('.custom-input-class')).toBeInTheDocument() }) it('should handle empty placeholder', () => { render() - expect(screen.getByRole('textbox'))!.toHaveAttribute('placeholder', '') + expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', '') }) it('should use default placeholder when not provided', () => { render() - expect(screen.getByRole('textbox'))!.toHaveAttribute('placeholder', '') + expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', '') }) }) @@ -392,14 +387,14 @@ describe('SearchBox', () => { it('should handle empty search value', () => { render() - expect(screen.getByRole('textbox'))!.toBeInTheDocument() - expect(screen.getByRole('textbox'))!.toHaveValue('') + expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox')).toHaveValue('') }) it('should handle empty tags array', () => { render() - expect(screen.getByTestId('portal-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-elem')).toBeInTheDocument() }) it('should handle special characters in search', () => { @@ -416,7 +411,7 @@ describe('SearchBox', () => { const longString = 'a'.repeat(1000) render() - expect(screen.getByDisplayValue(longString))!.toBeInTheDocument() + expect(screen.getByDisplayValue(longString)).toBeInTheDocument() }) it('should handle whitespace-only search', () => { @@ -444,21 +439,20 @@ describe('SearchBoxWrapper', () => { it('should render without crashing', () => { render() - expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should render in marketplace mode', () => { const { container } = render() - expect(container.querySelector('.rounded-xl'))!.toBeInTheDocument() + expect(container.querySelector('.rounded-xl')).toBeInTheDocument() }) it('should apply correct wrapper classes', () => { const { container } = render() // Check for z-11 class from wrapper - // Check for z-11 class from wrapper - expect(container.querySelector('.z-11'))!.toBeInTheDocument() + expect(container.querySelector('.z-11')).toBeInTheDocument() }) }) @@ -477,7 +471,7 @@ describe('SearchBoxWrapper', () => { it('should use translation for placeholder', () => { render() - expect(screen.getByPlaceholderText('Search plugins'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('Search plugins')).toBeInTheDocument() }) }) }) @@ -502,13 +496,13 @@ describe('MarketplaceTrigger', () => { it('should render without crashing', () => { render() - expect(screen.getByText('All Tags'))!.toBeInTheDocument() + expect(screen.getByText('All Tags')).toBeInTheDocument() }) it('should show "All Tags" when no tags selected', () => { render() - expect(screen.getByText('All Tags'))!.toBeInTheDocument() + expect(screen.getByText('All Tags')).toBeInTheDocument() }) it('should show arrow down icon when no tags selected', () => { @@ -517,8 +511,7 @@ describe('MarketplaceTrigger', () => { ) // Arrow down icon should be present - // Arrow down icon should be present - expect(container.querySelector('.size-4'))!.toBeInTheDocument() + expect(container.querySelector('.size-4')).toBeInTheDocument() }) }) @@ -532,7 +525,7 @@ describe('MarketplaceTrigger', () => { />, ) - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) it('should show multiple tag labels separated by comma', () => { @@ -544,7 +537,7 @@ describe('MarketplaceTrigger', () => { />, ) - expect(screen.getByText('Agent,RAG'))!.toBeInTheDocument() + expect(screen.getByText('Agent,RAG')).toBeInTheDocument() }) it('should show +N indicator when more than 2 tags selected', () => { @@ -556,7 +549,7 @@ describe('MarketplaceTrigger', () => { />, ) - expect(screen.getByText('+2'))!.toBeInTheDocument() + expect(screen.getByText('+2')).toBeInTheDocument() }) it('should only show first 2 tags in label', () => { @@ -568,7 +561,7 @@ describe('MarketplaceTrigger', () => { />, ) - expect(screen.getByText('Agent,RAG'))!.toBeInTheDocument() + expect(screen.getByText('Agent,RAG')).toBeInTheDocument() expect(screen.queryByText('Search')).not.toBeInTheDocument() }) }) @@ -584,8 +577,7 @@ describe('MarketplaceTrigger', () => { ) // RiCloseCircleFill icon should be present - // RiCloseCircleFill icon should be present - expect(container.querySelector('.text-text-quaternary'))!.toBeInTheDocument() + expect(container.querySelector('.text-text-quaternary')).toBeInTheDocument() }) it('should not show clear button when no tags selected', () => { @@ -593,37 +585,6 @@ describe('MarketplaceTrigger', () => { , ) - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present - // Clear button should not be present // Clear button should not be present expect(container.querySelector('.text-text-quaternary')).not.toBeInTheDocument() }) @@ -653,7 +614,7 @@ describe('MarketplaceTrigger', () => { , ) - expect(container.querySelector('.bg-state-base-hover'))!.toBeInTheDocument() + expect(container.querySelector('.bg-state-base-hover')).toBeInTheDocument() }) it('should apply border styling when tags are selected', () => { @@ -665,7 +626,7 @@ describe('MarketplaceTrigger', () => { />, ) - expect(container.querySelector('.border-components-button-secondary-border'))!.toBeInTheDocument() + expect(container.querySelector('.border-components-button-secondary-border')).toBeInTheDocument() }) }) @@ -675,7 +636,7 @@ describe('MarketplaceTrigger', () => { , ) - expect(container)!.toBeInTheDocument() + expect(container).toBeInTheDocument() }) }) }) @@ -700,13 +661,13 @@ describe('ToolSelectorTrigger', () => { it('should render without crashing', () => { const { container } = render() - expect(container)!.toBeInTheDocument() + expect(container).toBeInTheDocument() }) it('should render price tag icon', () => { const { container } = render() - expect(container.querySelector('.size-4'))!.toBeInTheDocument() + expect(container.querySelector('.size-4')).toBeInTheDocument() }) }) @@ -720,7 +681,7 @@ describe('ToolSelectorTrigger', () => { />, ) - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) it('should show multiple tag labels separated by comma', () => { @@ -732,7 +693,7 @@ describe('ToolSelectorTrigger', () => { />, ) - expect(screen.getByText('Agent,RAG'))!.toBeInTheDocument() + expect(screen.getByText('Agent,RAG')).toBeInTheDocument() }) it('should show +N indicator when more than 2 tags selected', () => { @@ -744,7 +705,7 @@ describe('ToolSelectorTrigger', () => { />, ) - expect(screen.getByText('+2'))!.toBeInTheDocument() + expect(screen.getByText('+2')).toBeInTheDocument() }) it('should not show tag labels when no tags selected', () => { @@ -764,7 +725,7 @@ describe('ToolSelectorTrigger', () => { />, ) - expect(container.querySelector('.text-text-quaternary'))!.toBeInTheDocument() + expect(container.querySelector('.text-text-quaternary')).toBeInTheDocument() }) it('should not show clear button when no tags selected', () => { @@ -824,7 +785,7 @@ describe('ToolSelectorTrigger', () => { , ) - expect(container.querySelector('.bg-state-base-hover'))!.toBeInTheDocument() + expect(container.querySelector('.bg-state-base-hover')).toBeInTheDocument() }) it('should apply border styling when tags are selected', () => { @@ -836,7 +797,7 @@ describe('ToolSelectorTrigger', () => { />, ) - expect(container.querySelector('.border-components-button-secondary-border'))!.toBeInTheDocument() + expect(container.querySelector('.border-components-button-secondary-border')).toBeInTheDocument() }) it('should not apply hover styling when open but has tags', () => { @@ -850,8 +811,7 @@ describe('ToolSelectorTrigger', () => { ) // Should have border styling, not hover - // Should have border styling, not hover - expect(container.querySelector('.border-components-button-secondary-border'))!.toBeInTheDocument() + expect(container.querySelector('.border-components-button-secondary-border')).toBeInTheDocument() }) }) @@ -866,7 +826,7 @@ describe('ToolSelectorTrigger', () => { />, ) - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) }) }) @@ -894,7 +854,7 @@ describe('TagsFilter', () => { />, ) - expect(screen.getByTestId('portal-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-elem')).toBeInTheDocument() }) it('should pass usedInMarketplace prop to TagsFilter', () => { @@ -909,8 +869,7 @@ describe('TagsFilter', () => { ) // MarketplaceTrigger should show "All Tags" - // MarketplaceTrigger should show "All Tags" - expect(screen.getByText('All Tags'))!.toBeInTheDocument() + expect(screen.getByText('All Tags')).toBeInTheDocument() }) it('should show selected tags count in TagsFilter trigger', () => { @@ -924,7 +883,7 @@ describe('TagsFilter', () => { />, ) - expect(screen.getByText('+1'))!.toBeInTheDocument() + expect(screen.getByText('+1')).toBeInTheDocument() }) }) @@ -943,7 +902,7 @@ describe('TagsFilter', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) }) @@ -962,7 +921,7 @@ describe('TagsFilter', () => { // Open fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) // Close @@ -988,8 +947,8 @@ describe('TagsFilter', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByText('Agent'))!.toBeInTheDocument() - expect(screen.getByText('RAG'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() + expect(screen.getByText('RAG')).toBeInTheDocument() }) }) @@ -1008,7 +967,7 @@ describe('TagsFilter', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) const agentOption = screen.getByText('Agent') @@ -1059,7 +1018,7 @@ describe('TagsFilter', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByText('RAG'))!.toBeInTheDocument() + expect(screen.getByText('RAG')).toBeInTheDocument() }) const ragOption = screen.getByText('RAG') @@ -1102,7 +1061,7 @@ describe('TagsFilter', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) const inputs = screen.getAllByRole('textbox') @@ -1112,7 +1071,7 @@ describe('TagsFilter', () => { if (searchInput) { fireEvent.change(searchInput, { target: { value: 'agent' } }) - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() } }) }) @@ -1138,8 +1097,7 @@ describe('TagsFilter', () => { }) // Verify dropdown content is rendered - // Verify dropdown content is rendered - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should render tag options when dropdown is open', async () => { @@ -1156,14 +1114,13 @@ describe('TagsFilter', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) // When no tags selected, these should appear once each in dropdown - // When no tags selected, these should appear once each in dropdown - expect(screen.getByText('Agent'))!.toBeInTheDocument() - expect(screen.getByText('RAG'))!.toBeInTheDocument() - expect(screen.getByText('Search'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() + expect(screen.getByText('RAG')).toBeInTheDocument() + expect(screen.getByText('Search')).toBeInTheDocument() }) }) }) @@ -1189,8 +1146,8 @@ describe('Accessibility', () => { ) const input = screen.getByRole('textbox') - expect(input)!.toBeInTheDocument() - expect(input)!.toHaveAttribute('placeholder', 'Search plugins') + expect(input).toBeInTheDocument() + expect(input).toHaveAttribute('placeholder', 'Search plugins') }) it('should have clickable tag options in dropdown', async () => { @@ -1199,7 +1156,7 @@ describe('Accessibility', () => { fireEvent.click(screen.getByTestId('portal-trigger')) await waitFor(() => { - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) }) }) @@ -1235,7 +1192,7 @@ describe('Combined Workflows', () => { fireEvent.click(trigger) await waitFor(() => { - expect(screen.getByText('Agent'))!.toBeInTheDocument() + expect(screen.getByText('Agent')).toBeInTheDocument() }) const agentOption = screen.getByText('Agent') @@ -1260,9 +1217,9 @@ describe('Combined Workflows', () => { />, ) - expect(screen.getByDisplayValue('test'))!.toBeInTheDocument() - expect(screen.getByText('Agent,RAG'))!.toBeInTheDocument() - expect(screen.getByTestId('portal-elem'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('test')).toBeInTheDocument() + expect(screen.getByText('Agent,RAG')).toBeInTheDocument() + expect(screen.getByTestId('portal-elem')).toBeInTheDocument() }) it('should handle prop changes correctly', () => { @@ -1277,7 +1234,7 @@ describe('Combined Workflows', () => { />, ) - expect(screen.getByDisplayValue('initial'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('initial')).toBeInTheDocument() rerender( { />, ) - expect(screen.getByDisplayValue('updated'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('updated')).toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx index 63cf25d039..8e04db6299 100644 --- a/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/__tests__/detail-header.spec.tsx @@ -297,13 +297,13 @@ describe('DetailHeader', () => { it('should render plugin title', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should render plugin icon with correct src', () => { render() - expect(screen.getByTestId('card-icon'))!.toBeInTheDocument() + expect(screen.getByTestId('card-icon')).toBeInTheDocument() }) it('should render icon with http url directly', () => { @@ -315,13 +315,13 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('card-icon'))!.toHaveAttribute('data-src', 'https://example.com/icon.png') + expect(screen.getByTestId('card-icon')).toHaveAttribute('data-src', 'https://example.com/icon.png') }) it('should render description when not in readme view', () => { render() - expect(screen.getByTestId('description'))!.toBeInTheDocument() + expect(screen.getByTestId('description')).toBeInTheDocument() }) it('should not render description in readme view', () => { @@ -333,7 +333,7 @@ describe('DetailHeader', () => { it('should render verified badge when verified', () => { render() - expect(screen.getByTestId('verified-badge'))!.toBeInTheDocument() + expect(screen.getByTestId('verified-badge')).toBeInTheDocument() }) }) @@ -346,8 +346,7 @@ describe('DetailHeader', () => { render() // Badge component should render with the version - // Badge component should render with the version - expect(screen.getByText('1.0.0'))!.toBeInTheDocument() + expect(screen.getByText('1.0.0')).toBeInTheDocument() }) it('should not show new version indicator when versions match', () => { @@ -358,8 +357,7 @@ describe('DetailHeader', () => { render() // Badge component should render with the version - // Badge component should render with the version - expect(screen.getByText('1.0.0'))!.toBeInTheDocument() + expect(screen.getByText('1.0.0')).toBeInTheDocument() }) it('should show update button when new version is available', () => { @@ -369,7 +367,7 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByText('plugin.detailPanel.operation.update'))!.toBeInTheDocument() + expect(screen.getByText('plugin.detailPanel.operation.update')).toBeInTheDocument() }) it('should show update button for GitHub source', () => { @@ -379,7 +377,7 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByText('plugin.detailPanel.operation.update'))!.toBeInTheDocument() + expect(screen.getByText('plugin.detailPanel.operation.update')).toBeInTheDocument() }) }) @@ -395,7 +393,7 @@ describe('DetailHeader', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should render component when strategy is disabled', () => { @@ -409,7 +407,7 @@ describe('DetailHeader', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should enable auto upgrade for update_all mode', () => { @@ -424,8 +422,7 @@ describe('DetailHeader', () => { render() // Auto upgrade badge should be rendered - // Auto upgrade badge should be rendered - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should enable auto upgrade for partial mode when plugin is included', () => { @@ -439,7 +436,7 @@ describe('DetailHeader', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should not enable auto upgrade for partial mode when plugin is not included', () => { @@ -453,7 +450,7 @@ describe('DetailHeader', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should enable auto upgrade for exclude mode when plugin is not excluded', () => { @@ -467,7 +464,7 @@ describe('DetailHeader', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should not enable auto upgrade for exclude mode when plugin is excluded', () => { @@ -481,7 +478,7 @@ describe('DetailHeader', () => { render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should not enable auto upgrade for non-marketplace plugins', () => { @@ -499,7 +496,7 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should not enable auto upgrade when marketplace feature is disabled', () => { @@ -515,8 +512,7 @@ describe('DetailHeader', () => { render() // Component should still render but auto upgrade should be disabled - // Component should still render but auto upgrade should be disabled - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) }) @@ -526,7 +522,7 @@ describe('DetailHeader', () => { // Find the close button (ActionButton with action-btn class) const actionButtons = screen.getAllByRole('button').filter(btn => btn.classList.contains('action-btn')) - fireEvent.click(actionButtons[actionButtons.length - 1]!) + fireEvent.click(actionButtons[actionButtons.length - 1]) expect(mockOnHide).toHaveBeenCalled() }) @@ -537,7 +533,7 @@ describe('DetailHeader', () => { const infoBtn = screen.getByTestId('info-btn') fireEvent.click(infoBtn) - expect(infoBtn)!.toBeInTheDocument() + expect(infoBtn).toBeInTheDocument() }) it('should have check version button available', () => { @@ -546,7 +542,7 @@ describe('DetailHeader', () => { const checkBtn = screen.getByTestId('check-version-btn') fireEvent.click(checkBtn) - expect(checkBtn)!.toBeInTheDocument() + expect(checkBtn).toBeInTheDocument() }) }) @@ -561,7 +557,7 @@ describe('DetailHeader', () => { const updateBtn = screen.getByText('plugin.detailPanel.operation.update') fireEvent.click(updateBtn) - expect(updateBtn)!.toBeInTheDocument() + expect(updateBtn).toBeInTheDocument() }) it('should have version picker select button', () => { @@ -570,7 +566,7 @@ describe('DetailHeader', () => { const selectBtn = screen.getByTestId('select-version-btn') fireEvent.click(selectBtn) - expect(selectBtn)!.toBeInTheDocument() + expect(selectBtn).toBeInTheDocument() }) it('should have downgrade button', () => { @@ -579,7 +575,7 @@ describe('DetailHeader', () => { const downgradeBtn = screen.getByTestId('select-downgrade-btn') fireEvent.click(downgradeBtn) - expect(downgradeBtn)!.toBeInTheDocument() + expect(downgradeBtn).toBeInTheDocument() }) }) @@ -655,7 +651,7 @@ describe('DetailHeader', () => { const removeBtn = screen.getByTestId('remove-btn') fireEvent.click(removeBtn) - expect(removeBtn)!.toBeInTheDocument() + expect(removeBtn).toBeInTheDocument() }) it('should have uninstallPlugin mock defined', () => { @@ -675,13 +671,13 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('remove-btn'))!.toBeInTheDocument() + expect(screen.getByTestId('remove-btn')).toBeInTheDocument() }) it('should render correctly for tool plugin delete', () => { render() - expect(screen.getByTestId('remove-btn'))!.toBeInTheDocument() + expect(screen.getByTestId('remove-btn')).toBeInTheDocument() }) }) @@ -693,21 +689,21 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should render local source icon', () => { const detail = createPluginDetail({ source: PluginSource.local }) render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should render debugging source icon', () => { const detail = createPluginDetail({ source: PluginSource.debugging }) render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should not render deprecation notice for non-marketplace source', () => { @@ -726,20 +722,20 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('operation-dropdown'))!.toBeInTheDocument() + expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument() }) it('should render marketplace source correctly', () => { render() - expect(screen.getByTestId('operation-dropdown'))!.toBeInTheDocument() + expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument() }) it('should render local source correctly', () => { const detail = createPluginDetail({ source: PluginSource.local }) render() - expect(screen.getByTestId('operation-dropdown'))!.toBeInTheDocument() + expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument() }) }) @@ -747,7 +743,7 @@ describe('DetailHeader', () => { it('should render plugin auth for tool category', () => { render() - expect(screen.getByTestId('plugin-auth'))!.toBeInTheDocument() + expect(screen.getByTestId('plugin-auth')).toBeInTheDocument() }) it('should not render plugin auth for non-tool category', () => { @@ -774,7 +770,7 @@ describe('DetailHeader', () => { const detail = createPluginDetail({ version: '' }) render() - expect(screen.getByTestId('title'))!.toBeInTheDocument() + expect(screen.getByTestId('title')).toBeInTheDocument() }) it('should handle plugin with name containing slash', () => { @@ -786,7 +782,7 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('org-info'))!.toBeInTheDocument() + expect(screen.getByTestId('org-info')).toBeInTheDocument() }) it('should handle empty icon', () => { @@ -798,7 +794,7 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('card-icon'))!.toHaveAttribute('data-src', '') + expect(screen.getByTestId('card-icon')).toHaveAttribute('data-src', '') }) }) @@ -809,7 +805,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) }) @@ -818,7 +814,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) @@ -833,7 +829,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) @@ -848,7 +844,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) @@ -869,7 +865,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) @@ -884,7 +880,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) @@ -899,7 +895,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('remove-btn')) await waitFor(() => { - expect(screen.getByRole('alertdialog'))!.toBeInTheDocument() + expect(screen.getByRole('alertdialog')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) @@ -921,7 +917,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByText('plugin.detailPanel.operation.update')) await waitFor(() => { - expect(screen.getByTestId('update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('update-modal')).toBeInTheDocument() }) }) @@ -934,7 +930,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByText('plugin.detailPanel.operation.update')) await waitFor(() => { - expect(screen.getByTestId('update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('update-modal')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('update-modal-save')) @@ -953,7 +949,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByText('plugin.detailPanel.operation.update')) await waitFor(() => { - expect(screen.getByTestId('update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('update-modal')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('update-modal-cancel')) @@ -971,7 +967,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('info-btn')) await waitFor(() => { - expect(screen.getByTestId('plugin-info'))!.toBeInTheDocument() + expect(screen.getByTestId('plugin-info')).toBeInTheDocument() }) }) @@ -980,7 +976,7 @@ describe('DetailHeader', () => { fireEvent.click(screen.getByTestId('info-btn')) await waitFor(() => { - expect(screen.getByTestId('plugin-info'))!.toBeInTheDocument() + expect(screen.getByTestId('plugin-info')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('plugin-info-close')) @@ -997,7 +993,7 @@ describe('DetailHeader', () => { }) render() - expect(screen.getByTestId('info-btn'))!.toBeInTheDocument() + expect(screen.getByTestId('info-btn')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx index f7dd1921e4..4dd604a03e 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx @@ -393,20 +393,19 @@ describe('AppTrigger', () => { it('should render placeholder when no app is selected', () => { render() // i18n mock returns key with namespace in dot format - // i18n mock returns key with namespace in dot format - expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.placeholder')).toBeInTheDocument() }) it('should render app details when app is selected', () => { const app = createMockApp({ name: 'My Test App' }) render() - expect(screen.getByText('My Test App'))!.toBeInTheDocument() + expect(screen.getByText('My Test App')).toBeInTheDocument() }) it('should apply open state styling', () => { const { container } = render() const trigger = container.querySelector('.bg-state-base-hover-alt') - expect(trigger)!.toBeInTheDocument() + expect(trigger).toBeInTheDocument() }) it('should render AppIcon when app is provided', () => { @@ -414,21 +413,21 @@ describe('AppTrigger', () => { const { container } = render() // AppIcon renders with a specific class when app is provided const iconContainer = container.querySelector('.mr-2') - expect(iconContainer)!.toBeInTheDocument() + expect(iconContainer).toBeInTheDocument() }) }) describe('Props', () => { it('should handle undefined appDetail gracefully', () => { render() - expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.placeholder')).toBeInTheDocument() }) it('should display app name with title attribute', () => { const app = createMockApp({ name: 'Long App Name For Testing' }) render() const nameElement = screen.getByTitle('Long App Name For Testing') - expect(nameElement)!.toBeInTheDocument() + expect(nameElement).toBeInTheDocument() }) }) @@ -436,14 +435,14 @@ describe('AppTrigger', () => { it('should have correct base classes', () => { const { container } = render() const trigger = container.firstChild as HTMLElement - expect(trigger)!.toHaveClass('group', 'flex', 'cursor-pointer') + expect(trigger).toHaveClass('group', 'flex', 'cursor-pointer') }) it('should apply different padding when app is provided', () => { const app = createMockApp() const { container } = render() const trigger = container.firstChild as HTMLElement - expect(trigger)!.toHaveClass('py-1.5', 'pl-1.5') + expect(trigger).toHaveClass('py-1.5', 'pl-1.5') }) }) }) @@ -480,18 +479,18 @@ describe('AppPicker', () => { describe('Rendering', () => { it('should render trigger element', () => { render() - expect(screen.getByText('Select App'))!.toBeInTheDocument() + expect(screen.getByText('Select App')).toBeInTheDocument() }) it('should render app list when open', () => { render() - expect(screen.getByText('App 1'))!.toBeInTheDocument() - expect(screen.getByText('App 2'))!.toBeInTheDocument() + expect(screen.getByText('App 1')).toBeInTheDocument() + expect(screen.getByText('App 2')).toBeInTheDocument() }) it('should show loading indicator when isLoading is true', () => { render() - expect(screen.getByText('common.loading'))!.toBeInTheDocument() + expect(screen.getByText('common.loading')).toBeInTheDocument() }) it('should not render content when isShow is false', () => { @@ -539,31 +538,31 @@ describe('AppPicker', () => { it('should display correct app type for CHAT', () => { const apps = [createMockApp({ id: 'chat-app', name: 'Chat App', mode: AppModeEnum.CHAT })] render() - expect(screen.getByText('chat'))!.toBeInTheDocument() + expect(screen.getByText('chat')).toBeInTheDocument() }) it('should display correct app type for WORKFLOW', () => { const apps = [createMockApp({ id: 'workflow-app', name: 'Workflow App', mode: AppModeEnum.WORKFLOW })] render() - expect(screen.getByText('workflow'))!.toBeInTheDocument() + expect(screen.getByText('workflow')).toBeInTheDocument() }) it('should display correct app type for ADVANCED_CHAT', () => { const apps = [createMockApp({ id: 'chatflow-app', name: 'Chatflow App', mode: AppModeEnum.ADVANCED_CHAT })] render() - expect(screen.getByText('chatflow'))!.toBeInTheDocument() + expect(screen.getByText('chatflow')).toBeInTheDocument() }) it('should display correct app type for AGENT_CHAT', () => { const apps = [createMockApp({ id: 'agent-app', name: 'Agent App', mode: AppModeEnum.AGENT_CHAT })] render() - expect(screen.getByText('agent'))!.toBeInTheDocument() + expect(screen.getByText('agent')).toBeInTheDocument() }) it('should display correct app type for COMPLETION', () => { const apps = [createMockApp({ id: 'completion-app', name: 'Completion App', mode: AppModeEnum.COMPLETION })] render() - expect(screen.getByText('completion'))!.toBeInTheDocument() + expect(screen.getByText('completion')).toBeInTheDocument() }) }) @@ -576,7 +575,7 @@ describe('AppPicker', () => { it('should handle search text with value', () => { render() const input = screen.getByTestId('input') - expect(input)!.toHaveValue('test search') + expect(input).toHaveValue('test search') }) }) @@ -642,8 +641,7 @@ describe('AppPicker', () => { render() // The component should render without errors - // The component should render without errors - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle isShow toggle correctly', () => { @@ -656,8 +654,7 @@ describe('AppPicker', () => { rerender() // Should not crash - // Should not crash - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should setup intersection observer when isShow is true', () => { @@ -677,8 +674,7 @@ describe('AppPicker', () => { rerender() // Component should render without errors - // Component should render without errors - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should cleanup observer on component unmount', () => { @@ -695,8 +691,7 @@ describe('AppPicker', () => { triggerMutationObserver() // Component should still work correctly - // Component should still work correctly - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should not setup IntersectionObserver when observerTarget is null', () => { @@ -704,8 +699,7 @@ describe('AppPicker', () => { render() // The guard at line 84 should prevent setup - // The guard at line 84 should prevent setup - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should debounce onLoadMore calls using loadingRef', () => { @@ -804,8 +798,8 @@ describe('AppInputsForm', () => { { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false }, ] render() - expect(screen.getByText('Name'))!.toBeInTheDocument() - expect(screen.getByPlaceholderText('Name'))!.toBeInTheDocument() + expect(screen.getByText('Name')).toBeInTheDocument() + expect(screen.getByPlaceholderText('Name')).toBeInTheDocument() }) it('should render number input field', () => { @@ -813,7 +807,7 @@ describe('AppInputsForm', () => { { type: InputVarType.number, label: 'Count', variable: 'count', required: false }, ] render() - expect(screen.getByText('Count'))!.toBeInTheDocument() + expect(screen.getByText('Count')).toBeInTheDocument() }) it('should render paragraph (textarea) field', () => { @@ -821,7 +815,7 @@ describe('AppInputsForm', () => { { type: InputVarType.paragraph, label: 'Description', variable: 'desc', required: false }, ] render() - expect(screen.getByText('Description'))!.toBeInTheDocument() + expect(screen.getByText('Description')).toBeInTheDocument() }) it('should render select field', () => { @@ -846,8 +840,8 @@ describe('AppInputsForm', () => { }, ] render() - expect(screen.getByText('Single File Upload'))!.toBeInTheDocument() - expect(screen.getByTestId('file-uploader'))!.toBeInTheDocument() + expect(screen.getByText('Single File Upload')).toBeInTheDocument() + expect(screen.getByTestId('file-uploader')).toBeInTheDocument() }) it('should render file uploader for single file with existing value', () => { @@ -865,8 +859,7 @@ describe('AppInputsForm', () => { ] render() // The file uploader should receive the existing file as an array - // The file uploader should receive the existing file as an array - expect(screen.getByTestId('file-value'))!.toHaveTextContent(JSON.stringify([existingFile])) + expect(screen.getByTestId('file-value')).toHaveTextContent(JSON.stringify([existingFile])) }) it('should render file uploader for multi files', () => { @@ -883,7 +876,7 @@ describe('AppInputsForm', () => { }, ] render() - expect(screen.getByText('Attachments'))!.toBeInTheDocument() + expect(screen.getByText('Attachments')).toBeInTheDocument() }) it('should show optional label for non-required fields', () => { @@ -891,7 +884,7 @@ describe('AppInputsForm', () => { { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false }, ] render() - expect(screen.getByText('workflow.panel.optional'))!.toBeInTheDocument() + expect(screen.getByText('workflow.panel.optional')).toBeInTheDocument() }) it('should not show optional label for required fields', () => { @@ -1033,7 +1026,7 @@ describe('AppInputsForm', () => { render() const input = screen.getByPlaceholderText('Name') - expect(input)!.toHaveValue('existing') + expect(input).toHaveValue('existing') }) it('should handle empty string value', () => { @@ -1043,7 +1036,7 @@ describe('AppInputsForm', () => { render() const input = screen.getByPlaceholderText('Name') - expect(input)!.toHaveValue('') + expect(input).toHaveValue('') }) it('should handle undefined variable value', () => { @@ -1053,7 +1046,7 @@ describe('AppInputsForm', () => { render() const input = screen.getByPlaceholderText('Name') - expect(input)!.toHaveValue('') + expect(input).toHaveValue('') }) it('should handle multiple form fields', () => { @@ -1064,9 +1057,9 @@ describe('AppInputsForm', () => { ] render() - expect(screen.getByText('Name'))!.toBeInTheDocument() - expect(screen.getByText('Age'))!.toBeInTheDocument() - expect(screen.getByText('Bio'))!.toBeInTheDocument() + expect(screen.getByText('Name')).toBeInTheDocument() + expect(screen.getByText('Age')).toBeInTheDocument() + expect(screen.getByText('Bio')).toBeInTheDocument() }) it('should handle unknown form type gracefully', () => { @@ -1075,7 +1068,7 @@ describe('AppInputsForm', () => { ] // Should not throw error, just not render the field render() - expect(screen.getByText('Unknown'))!.toBeInTheDocument() + expect(screen.getByText('Unknown')).toBeInTheDocument() }) }) }) @@ -1100,49 +1093,18 @@ describe('AppInputsPanel', () => { describe('Rendering', () => { it('should render without crashing', () => { renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should show no params message when form schema is empty', () => { renderWithQueryClient() - expect(screen.getByText('app.appSelector.noParams'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.noParams')).toBeInTheDocument() }) it('should show loading state when app is loading', () => { mockAppDetailLoading = true renderWithQueryClient() // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered - // Loading component should be rendered expect(screen.queryByText('app.appSelector.params')).not.toBeInTheDocument() }) @@ -1157,19 +1119,19 @@ describe('AppInputsPanel', () => { describe('Props', () => { it('should handle undefined value', () => { renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should handle different app modes', () => { const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should handle advanced chat mode', () => { const advancedChatApp = createMockApp({ mode: AppModeEnum.ADVANCED_CHAT }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) }) @@ -1185,7 +1147,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for number input', () => { @@ -1199,7 +1161,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for checkbox input', () => { @@ -1213,7 +1175,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for select input', () => { @@ -1227,7 +1189,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for file-list input', () => { @@ -1241,7 +1203,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for file input', () => { @@ -1255,7 +1217,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for json_object input', () => { @@ -1269,7 +1231,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for text-input (default)', () => { @@ -1283,7 +1245,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should filter external_data_tool items', () => { @@ -1298,7 +1260,7 @@ describe('AppInputsPanel', () => { }, }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) }) @@ -1321,7 +1283,7 @@ describe('AppInputsPanel', () => { } const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for workflow with singleFile variable', () => { @@ -1342,7 +1304,7 @@ describe('AppInputsPanel', () => { } const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should generate schema for workflow with regular variable', () => { @@ -1363,7 +1325,7 @@ describe('AppInputsPanel', () => { } const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) }) @@ -1382,7 +1344,7 @@ describe('AppInputsPanel', () => { }) const completionApp = createMockApp({ mode: AppModeEnum.COMPLETION }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should add image upload schema for WORKFLOW mode with file upload enabled', () => { @@ -1402,7 +1364,7 @@ describe('AppInputsPanel', () => { } const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) }) @@ -1410,7 +1372,7 @@ describe('AppInputsPanel', () => { it('should call onFormChange when form is updated', () => { const onFormChange = vi.fn() renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) it('should call onFormChange with updated values when text input changes', () => { @@ -1462,7 +1424,7 @@ describe('AppInputsPanel', () => { , ) - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) }) @@ -1470,7 +1432,7 @@ describe('AppInputsPanel', () => { it('should return empty schema when currentApp is null', () => { mockAppDetailData = null renderWithQueryClient() - expect(screen.getByText('app.appSelector.noParams'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.noParams')).toBeInTheDocument() }) it('should handle workflow without start node', () => { @@ -1480,7 +1442,7 @@ describe('AppInputsPanel', () => { } const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) renderWithQueryClient() - expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.params')).toBeInTheDocument() }) }) }) @@ -1515,12 +1477,12 @@ describe('AppSelector', () => { describe('Rendering', () => { it('should render without crashing', () => { renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should render trigger component', () => { renderWithQueryClient() - expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.placeholder')).toBeInTheDocument() }) it('should show selected app info when value is provided', () => { @@ -1531,20 +1493,19 @@ describe('AppSelector', () => { />, ) // Should show the app trigger with app info - // Should show the app trigger with app info - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) describe('Props', () => { it('should handle different placement values', () => { renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle different offset values', () => { renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle disabled state', () => { @@ -1552,13 +1513,12 @@ describe('AppSelector', () => { const trigger = screen.getByTestId('portal-trigger') fireEvent.click(trigger) // Portal should remain closed when disabled - // Portal should remain closed when disabled - expect(screen.getByTestId('portal-to-follow-elem'))!.toHaveAttribute('data-open', 'false') + expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false') }) it('should handle scope prop', () => { renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle value with inputs', () => { @@ -1568,7 +1528,7 @@ describe('AppSelector', () => { value={{ app_id: 'app-1', inputs: { name: 'test' }, files: [] }} />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle value with files', () => { @@ -1578,7 +1538,7 @@ describe('AppSelector', () => { value={{ app_id: 'app-1', inputs: {}, files: [{ id: 'file-1' }] }} />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) @@ -1587,11 +1547,10 @@ describe('AppSelector', () => { renderWithQueryClient() const trigger = screen.getAllByTestId('portal-trigger')[0] - fireEvent.click(trigger!) + fireEvent.click(trigger) // The portal state should update synchronously - get the first one (outer portal) - // The portal state should update synchronously - get the first one (outer portal) - expect(screen.getAllByTestId('portal-to-follow-elem')[0])!.toHaveAttribute('data-open', 'true') + expect(screen.getAllByTestId('portal-to-follow-elem')[0]).toHaveAttribute('data-open', 'true') }) it('should not toggle isShow when disabled', () => { @@ -1600,7 +1559,7 @@ describe('AppSelector', () => { const trigger = screen.getByTestId('portal-trigger') fireEvent.click(trigger) - expect(screen.getByTestId('portal-to-follow-elem'))!.toHaveAttribute('data-open', 'false') + expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false') }) it('should manage search text state', () => { @@ -1610,8 +1569,7 @@ describe('AppSelector', () => { fireEvent.click(trigger) // Portal content should be visible after click - // Portal content should be visible after click - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should render correctly during load more setup', () => { @@ -1621,8 +1579,7 @@ describe('AppSelector', () => { renderWithQueryClient() // Trigger should be rendered - // Trigger should be rendered - expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() }) }) @@ -1635,7 +1592,7 @@ describe('AppSelector', () => { // Open the portal fireEvent.click(screen.getByTestId('portal-trigger')) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should call onSelect with correct value structure', () => { @@ -1649,8 +1606,7 @@ describe('AppSelector', () => { ) // The component should maintain the correct value structure - // The component should maintain the correct value structure - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should clear inputs when selecting different app', () => { @@ -1664,8 +1620,7 @@ describe('AppSelector', () => { ) // Component renders with existing value - // Component renders with existing value - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should preserve inputs when selecting same app', () => { @@ -1678,7 +1633,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) @@ -1692,7 +1647,7 @@ describe('AppSelector', () => { } renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should memoize currentAppInfo correctly', () => { @@ -1707,7 +1662,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should memoize formattedValue correctly', () => { @@ -1718,7 +1673,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should be wrapped with React.memo', () => { @@ -1735,7 +1690,7 @@ describe('AppSelector', () => { , ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) @@ -1743,7 +1698,7 @@ describe('AppSelector', () => { it('should handle load more when hasMore is true', async () => { mockHasNextPage = true renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should not trigger load more when already loading', async () => { @@ -1769,7 +1724,7 @@ describe('AppSelector', () => { vi.advanceTimersByTime(500) }) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should render load more area when hasMore is true', () => { @@ -1780,11 +1735,10 @@ describe('AppSelector', () => { renderWithQueryClient() // Open the portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Should render without errors - // Should render without errors - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should handle fetchNextPage rejection gracefully in handleLoadMore', async () => { @@ -1794,8 +1748,7 @@ describe('AppSelector', () => { renderWithQueryClient() // Should not crash even if fetchNextPage rejects - // Should not crash even if fetchNextPage rejects - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should call fetchNextPage when intersection observer triggers handleLoadMore', async () => { @@ -1806,11 +1759,11 @@ describe('AppSelector', () => { renderWithQueryClient() // Open the main portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Open the inner app picker portal const triggers = screen.getAllByTestId('portal-trigger') - fireEvent.click(triggers[1]!) + fireEvent.click(triggers[1]) // Simulate intersection to trigger handleLoadMore triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry]) @@ -1827,9 +1780,9 @@ describe('AppSelector', () => { renderWithQueryClient() // Open portals - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) const triggers = screen.getAllByTestId('portal-trigger') - fireEvent.click(triggers[1]!) + fireEvent.click(triggers[1]) // Trigger first intersection triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry]) @@ -1853,9 +1806,9 @@ describe('AppSelector', () => { renderWithQueryClient() // Open portals - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) const triggers = screen.getAllByTestId('portal-trigger') - fireEvent.click(triggers[1]!) + fireEvent.click(triggers[1]) // Trigger intersection triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry]) @@ -1872,9 +1825,9 @@ describe('AppSelector', () => { renderWithQueryClient() // Open portals - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) const triggers = screen.getAllByTestId('portal-trigger') - fireEvent.click(triggers[1]!) + fireEvent.click(triggers[1]) // Trigger intersection triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry]) @@ -1894,7 +1847,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle form change without image file', () => { @@ -1907,7 +1860,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should extract #image# from inputs and add to files array', () => { @@ -1921,7 +1874,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should preserve existing files when no #image# in inputs', () => { @@ -1934,7 +1887,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) @@ -1954,9 +1907,9 @@ describe('AppSelector', () => { ) // Open the main portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should preserve inputs when selecting the same app', () => { @@ -1973,7 +1926,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle app selection with empty value', () => { @@ -1991,34 +1944,34 @@ describe('AppSelector', () => { ) // Open the main portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) }) describe('Edge Cases', () => { it('should handle undefined value', () => { renderWithQueryClient() - expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.placeholder')).toBeInTheDocument() }) it('should handle empty pages array', () => { mockAppListData = { pages: [] } renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle undefined data', () => { mockAppListData = undefined renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle loading state', () => { mockIsLoading = true renderWithQueryClient() - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle app not found in displayedApps', () => { @@ -2033,7 +1986,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle value with empty inputs and files', () => { @@ -2044,7 +1997,7 @@ describe('AppSelector', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) @@ -2056,8 +2009,7 @@ describe('AppSelector', () => { renderWithQueryClient() // Should not crash - // Should not crash - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) }) @@ -2091,11 +2043,10 @@ describe('AppSelector Integration', () => { renderWithQueryClient() // 1. Click trigger to open picker - get first trigger (outer portal) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Get the first portal element (outer portal) - // Get the first portal element (outer portal) - expect(screen.getAllByTestId('portal-to-follow-elem')[0])!.toHaveAttribute('data-open', 'true') + expect(screen.getAllByTestId('portal-to-follow-elem')[0]).toHaveAttribute('data-open', 'true') }) it('should handle app change with input preservation logic', () => { @@ -2107,7 +2058,7 @@ describe('AppSelector Integration', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) }) @@ -2116,8 +2067,7 @@ describe('AppSelector Integration', () => { renderWithQueryClient() // AppTrigger should show placeholder when no app selected - // AppTrigger should show placeholder when no app selected - expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument() + expect(screen.getByText('app.appSelector.placeholder')).toBeInTheDocument() }) it('should pass correct props to AppPicker', () => { @@ -2125,7 +2075,7 @@ describe('AppSelector Integration', () => { fireEvent.click(screen.getByTestId('portal-trigger')) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) }) @@ -2138,7 +2088,7 @@ describe('AppSelector Integration', () => { />, ) - expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument() }) it('should handle search filtering through app list', () => { @@ -2146,7 +2096,7 @@ describe('AppSelector Integration', () => { fireEvent.click(screen.getByTestId('portal-trigger')) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) }) @@ -2165,13 +2115,13 @@ describe('AppSelector Integration', () => { ) // Open the main portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // The inner AppPicker portal is closed by default (isShowChooseApp = false) // We need to click on the inner trigger to open it const innerTriggers = screen.getAllByTestId('portal-trigger') // The second trigger is the inner AppPicker trigger - fireEvent.click(innerTriggers[1]!) + fireEvent.click(innerTriggers[1]) // Now the inner portal should be open and show the app list // Find and click on app-2 @@ -2200,16 +2150,16 @@ describe('AppSelector Integration', () => { ) // Open the main portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Click on the inner trigger to open app picker const innerTriggers = screen.getAllByTestId('portal-trigger') - fireEvent.click(innerTriggers[1]!) + fireEvent.click(innerTriggers[1]) // Click on the same app - need to get the one in the app list, not the trigger const appItems = screen.getAllByText('App 1') // The last one should be in the dropdown list - fireEvent.click(appItems[appItems.length - 1]!) + fireEvent.click(appItems[appItems.length - 1]) // onSelect should be called with preserved inputs since it's the same app expect(onSelect).toHaveBeenCalledWith({ @@ -2233,15 +2183,15 @@ describe('AppSelector Integration', () => { ) // Open the main portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Click on inner trigger to open app picker const innerTriggers = screen.getAllByTestId('portal-trigger') - fireEvent.click(innerTriggers[1]!) + fireEvent.click(innerTriggers[1]) // Click on an app from the dropdown const app1Elements = screen.getAllByText('App 1') - fireEvent.click(app1Elements[app1Elements.length - 1]!) + fireEvent.click(app1Elements[app1Elements.length - 1]) // onSelect should be called with new app and empty inputs/files expect(onSelect).toHaveBeenCalledWith({ @@ -2261,9 +2211,9 @@ describe('AppSelector Integration', () => { renderWithQueryClient() // Open the portal to render the app picker - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should stay stable after fetchNextPage completes', async () => { @@ -2273,9 +2223,9 @@ describe('AppSelector Integration', () => { renderWithQueryClient() - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should not call fetchNextPage when conditions prevent it', () => { @@ -2284,7 +2234,7 @@ describe('AppSelector Integration', () => { renderWithQueryClient() - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // fetchNextPage should not be called expect(mockFetchNextPage).not.toHaveBeenCalled() @@ -2306,7 +2256,7 @@ describe('AppSelector Integration', () => { ) // Open portal - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // formattedValue should include #image# from files expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0) @@ -2325,7 +2275,7 @@ describe('AppSelector Integration', () => { />, ) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0) }) @@ -2343,7 +2293,7 @@ describe('AppSelector Integration', () => { />, ) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0) }) @@ -2374,12 +2324,12 @@ describe('AppSelector Integration', () => { ) // Open portal to render AppInputsPanel - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Find and interact with the form input (may not exist if schema is empty) const formInputs = screen.queryAllByPlaceholderText('FormInputField') if (formInputs.length > 0) { - fireEvent.change(formInputs[0]!, { target: { value: 'test value' } }) + fireEvent.change(formInputs[0], { target: { value: 'test value' } }) // handleFormChange in index.tsx should have been called expect(onSelect).toHaveBeenCalledWith({ @@ -2426,12 +2376,12 @@ describe('AppSelector Integration', () => { />, ) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Find file uploader and trigger upload - the #image# field will be extracted const uploadBtns = screen.queryAllByTestId('upload-file-btn') if (uploadBtns.length > 0) { - fireEvent.click(uploadBtns[0]!) + fireEvent.click(uploadBtns[0]) // handleFormChange should extract #image# and convert to files expect(onSelect).toHaveBeenCalled() } @@ -2464,12 +2414,12 @@ describe('AppSelector Integration', () => { />, ) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Find form input (may not exist if schema is empty) const inputs = screen.queryAllByPlaceholderText('PreserveField') if (inputs.length > 0) { - fireEvent.change(inputs[0]!, { target: { value: 'updated name' } }) + fireEvent.change(inputs[0], { target: { value: 'updated name' } }) // onSelect should be called preserving existing files (no #image# in inputs) expect(onSelect).toHaveBeenCalledWith({ @@ -2515,7 +2465,7 @@ describe('AppSelector Integration', () => { />, ) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) // Try to find and click the upload button which triggers #image# form change const uploadBtn = screen.queryByTestId('upload-file-btn') @@ -2549,11 +2499,11 @@ describe('AppSelector Integration', () => { />, ) - fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!) + fireEvent.click(screen.getAllByTestId('portal-trigger')[0]) const inputs = screen.queryAllByPlaceholderText('SimpleInput') if (inputs.length > 0) { - fireEvent.change(inputs[0]!, { target: { value: 'changed' } }) + fireEvent.change(inputs[0], { target: { value: 'changed' } }) // handleFormChange should preserve existing files when no #image# in inputs expect(onSelect).toHaveBeenCalledWith({ app_id: 'app-1', diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx index 41140ac63b..c4cb4f4da8 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx @@ -78,7 +78,7 @@ const AppPicker: FC = ({ const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => { const target = entries[0] - if (!target!.isIntersecting || loadingRef.current || !hasMore || isLoading) + if (!target.isIntersecting || loadingRef.current || !hasMore || isLoading) return loadingRef.current = true @@ -188,7 +188,7 @@ const AppPicker: FC = ({ {apps.map(app => (
onSelect(app)} > = ({ background={app.icon_background} imageUrl={app.icon_url} /> -
+
{app.name} ( @@ -207,7 +207,7 @@ const AppPicker: FC = ({ )
-
{getAppType(app)}
+
{getAppType(app)}
))}
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx index 97e144af6f..abd47b4592 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx @@ -161,7 +161,7 @@ const AppSelector: FC = ({
-
{t('appSelector.label', { ns: 'app' })}
+
{t('appSelector.label', { ns: 'app' })}
{ const { container } = render() // Assert - // Assert - expect(container)!.toBeInTheDocument() + expect(container).toBeInTheDocument() }) it('should render language label', () => { @@ -173,8 +172,7 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByText('appDebug.voice.voiceSettings.language'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.voice.voiceSettings.language')).toBeInTheDocument() }) it('should render voice label', () => { @@ -185,8 +183,7 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByText('appDebug.voice.voiceSettings.voice'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.voice.voiceSettings.voice')).toBeInTheDocument() }) it('should render two Select components', () => { @@ -210,7 +207,7 @@ describe('TTSParamsPanel', () => { // Assert const values = screen.getAllByTestId('selected-value') - expect(values[0])!.toHaveTextContent('zh-Hans') + expect(values[0]).toHaveTextContent('zh-Hans') }) it('should render voice select with correct value', () => { @@ -222,7 +219,7 @@ describe('TTSParamsPanel', () => { // Assert const values = screen.getAllByTestId('selected-value') - expect(values[1])!.toHaveTextContent('echo') + expect(values[1]).toHaveTextContent('echo') }) it('should only show supported languages in language select', () => { @@ -233,10 +230,9 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('select-item-en-US'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-zh-Hans'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-ja-JP'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-en-US')).toBeInTheDocument() + expect(screen.getByTestId('select-item-zh-Hans')).toBeInTheDocument() + expect(screen.getByTestId('select-item-ja-JP')).toBeInTheDocument() expect(screen.queryByTestId('select-item-unsupported-lang')).not.toBeInTheDocument() }) @@ -248,10 +244,9 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-echo'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-fable'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument() + expect(screen.getByTestId('select-item-echo')).toBeInTheDocument() + expect(screen.getByTestId('select-item-fable')).toBeInTheDocument() }) }) @@ -265,9 +260,8 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('tts-language-select-trigger'))!.toHaveAttribute('data-class', 'w-full') - expect(screen.getByTestId('tts-voice-select-trigger'))!.toHaveAttribute('data-class', 'w-full') + expect(screen.getByTestId('tts-language-select-trigger')).toHaveAttribute('data-class', 'w-full') + expect(screen.getByTestId('tts-voice-select-trigger')).toHaveAttribute('data-class', 'w-full') }) it('should apply popup className to SelectContent', () => { @@ -279,8 +273,8 @@ describe('TTSParamsPanel', () => { // Assert const contents = screen.getAllByTestId('select-content') - expect(contents[0])!.toHaveAttribute('data-popup-class', 'w-[354px]') - expect(contents[1])!.toHaveAttribute('data-popup-class', 'w-[354px]') + expect(contents[0]).toHaveAttribute('data-popup-class', 'w-[354px]') + expect(contents[1]).toHaveAttribute('data-popup-class', 'w-[354px]') }) }) @@ -402,37 +396,6 @@ describe('TTSParamsPanel', () => { // Act render() - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered - // Assert - no voice items should be rendered // Assert - no voice items should be rendered expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument() expect(screen.queryByTestId('select-item-echo')).not.toBeInTheDocument() @@ -450,37 +413,6 @@ describe('TTSParamsPanel', () => { // Act render() - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert // Assert expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument() }) @@ -498,9 +430,8 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('select-item-voice-1'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-voice-2'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-voice-1')).toBeInTheDocument() + expect(screen.getByTestId('select-item-voice-2')).toBeInTheDocument() }) it('should handle currentModel with empty voices array', () => { @@ -513,7 +444,7 @@ describe('TTSParamsPanel', () => { render() // Assert - no voice items (except language items) - expect(screen.getAllByTestId('select-content')[1]!.children).toHaveLength(0) + expect(screen.getAllByTestId('select-content')[1].children).toHaveLength(0) expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument() }) @@ -529,8 +460,7 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('select-item-single-voice'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-single-voice')).toBeInTheDocument() }) }) @@ -545,7 +475,7 @@ describe('TTSParamsPanel', () => { // Assert const values = screen.getAllByTestId('selected-value') - expect(values[0])!.toHaveTextContent('') + expect(values[0]).toHaveTextContent('') }) it('should handle empty voice value', () => { @@ -557,7 +487,7 @@ describe('TTSParamsPanel', () => { // Assert const values = screen.getAllByTestId('selected-value') - expect(values[1])!.toHaveTextContent('') + expect(values[1]).toHaveTextContent('') }) it('should handle many voices', () => { @@ -574,9 +504,8 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('select-item-voice-0'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-voice-19'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-voice-0')).toBeInTheDocument() + expect(screen.getByTestId('select-item-voice-19')).toBeInTheDocument() }) it('should handle voice with special characters in mode', () => { @@ -591,8 +520,7 @@ describe('TTSParamsPanel', () => { render() // Assert - // Assert - expect(screen.getByTestId('select-item-voice-with_special.chars'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-voice-with_special.chars')).toBeInTheDocument() }) it('should handle onChange not being called multiple times', () => { @@ -618,13 +546,13 @@ describe('TTSParamsPanel', () => { // Act const { rerender } = render() const values = screen.getAllByTestId('selected-value') - expect(values[0])!.toHaveTextContent('en-US') + expect(values[0]).toHaveTextContent('en-US') rerender() // Assert const updatedValues = screen.getAllByTestId('selected-value') - expect(updatedValues[0])!.toHaveTextContent('zh-Hans') + expect(updatedValues[0]).toHaveTextContent('zh-Hans') }) it('should update when voice prop changes', () => { @@ -634,13 +562,13 @@ describe('TTSParamsPanel', () => { // Act const { rerender } = render() const values = screen.getAllByTestId('selected-value') - expect(values[1])!.toHaveTextContent('alloy') + expect(values[1]).toHaveTextContent('alloy') rerender() // Assert const updatedValues = screen.getAllByTestId('selected-value') - expect(updatedValues[1])!.toHaveTextContent('echo') + expect(updatedValues[1]).toHaveTextContent('echo') }) it('should update voice list when currentModel changes', () => { @@ -652,7 +580,7 @@ describe('TTSParamsPanel', () => { // Act const { rerender } = render() - expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument() expect(screen.queryByTestId('select-item-nova')).not.toBeInTheDocument() const newModel = createCurrentModel([ @@ -662,9 +590,8 @@ describe('TTSParamsPanel', () => { rerender() // Assert - // Assert - expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument() - expect(screen.getByTestId('select-item-nova'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument() + expect(screen.getByTestId('select-item-nova')).toBeInTheDocument() }) it('should handle currentModel becoming null', () => { @@ -673,41 +600,10 @@ describe('TTSParamsPanel', () => { // Act const { rerender } = render() - expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument() + expect(screen.getByTestId('select-item-alloy')).toBeInTheDocument() rerender() - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert // Assert expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument() }) @@ -740,7 +636,7 @@ describe('TTSParamsPanel', () => { // Assert const languageLabel = screen.getByText('appDebug.voice.voiceSettings.language') - expect(languageLabel)!.toHaveClass('system-sm-semibold') + expect(languageLabel).toHaveClass('system-sm-semibold') }) it('should have proper label structure for voice select', () => { @@ -752,7 +648,7 @@ describe('TTSParamsPanel', () => { // Assert const voiceLabel = screen.getByText('appDebug.voice.voiceSettings.voice') - expect(voiceLabel)!.toHaveClass('system-sm-semibold') + expect(voiceLabel).toHaveClass('system-sm-semibold') }) }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx index 19a2d5a9f1..d41dfaa7d0 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx @@ -100,8 +100,8 @@ describe('SubscriptionList', () => { it('should render list view by default', () => { render() - expect(screen.getByText(/pluginTrigger\.subscription\.listNum/))!.toBeInTheDocument() - expect(screen.getByText('Subscription One'))!.toBeInTheDocument() + expect(screen.getByText(/pluginTrigger\.subscription\.listNum/)).toBeInTheDocument() + expect(screen.getByText('Subscription One')).toBeInTheDocument() }) it('should render loading state when subscriptions are loading', () => { @@ -112,7 +112,7 @@ describe('SubscriptionList', () => { render() - expect(screen.getByRole('status'))!.toBeInTheDocument() + expect(screen.getByRole('status')).toBeInTheDocument() expect(screen.queryByText('Subscription One')).not.toBeInTheDocument() }) @@ -121,7 +121,7 @@ describe('SubscriptionList', () => { render() - expect(screen.getByText('Subscription One'))!.toBeInTheDocument() + expect(screen.getByText('Subscription One')).toBeInTheDocument() }) it('should render without list entries when subscriptions are empty', () => { @@ -141,7 +141,7 @@ describe('SubscriptionList', () => { it('should render selector view when mode is selector', () => { render() - expect(screen.getByText('Subscription One'))!.toBeInTheDocument() + expect(screen.getByText('Subscription One')).toBeInTheDocument() }) it('should visually distinguish selected subscription from unselected', () => { @@ -182,7 +182,7 @@ describe('SubscriptionList', () => { fireEvent.click(screen.getByRole('button', { name: 'Subscription One' })) expect(onSelect).toHaveBeenCalledTimes(1) - const [selectedSubscription, callback] = (onSelect.mock.calls[0] ?? []) as [any, any] + const [selectedSubscription, callback] = onSelect.mock.calls[0] expect(selectedSubscription).toMatchObject({ id: 'sub-1', name: 'Subscription One' }) expect(typeof callback).toBe('function') @@ -212,7 +212,7 @@ describe('SubscriptionList', () => { fireEvent.click(deleteButton) expect(onSelect).not.toHaveBeenCalled() - expect(screen.getByText(/pluginTrigger\.subscription\.list\.item\.actions\.deleteConfirm\.title/))!.toBeInTheDocument() + expect(screen.getByText(/pluginTrigger\.subscription\.list\.item\.actions\.deleteConfirm\.title/)).toBeInTheDocument() }) }) @@ -222,7 +222,7 @@ describe('SubscriptionList', () => { render() - expect(await screen.findByText('Something went wrong'))!.toBeInTheDocument() + expect(await screen.findByText('Something went wrong')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx index 1c731d5eca..d1eee1cfb5 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx @@ -137,7 +137,7 @@ const ReasoningConfigForm: React.FC = ({ asChild={false} /> )) - const varInput = value[variable]!.value + const varInput = value[variable].value const { isString, isNumber, @@ -179,7 +179,7 @@ const ReasoningConfigForm: React.FC = ({ >
showSchema(input_schema as SchemaRoot, fieldTitle!)} + onClick={() => showSchema(input_schema as SchemaRoot, fieldTitle)} >
diff --git a/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx b/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx index b0ecc839b3..f2dd70d41b 100644 --- a/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx +++ b/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx @@ -253,11 +253,10 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert - // Assert - expect(screen.getByText('plugin.action.delete'))!.toBeInTheDocument() + expect(screen.getByText('plugin.action.delete')).toBeInTheDocument() }) it('should display plugin name in delete confirm content', () => { @@ -271,11 +270,10 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert - // Assert - expect(screen.getByText('my-awesome-plugin'))!.toBeInTheDocument() + expect(screen.getByText('my-awesome-plugin')).toBeInTheDocument() }) it('should hide confirm modal when cancel is clicked', () => { @@ -288,8 +286,8 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) - expect(screen.getByText('plugin.action.delete'))!.toBeInTheDocument() + fireEvent.click(getActionButtons()[0]) + expect(screen.getByText('plugin.action.delete')).toBeInTheDocument() fireEvent.click(getDeleteCancelButton()) @@ -310,7 +308,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) // Assert @@ -332,7 +330,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) // Assert @@ -354,7 +352,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) // Assert @@ -376,7 +374,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) // Assert @@ -403,12 +401,12 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) // Assert - Loading state await waitFor(() => { - expect(getDeleteConfirmButton())!.toBeDisabled() + expect(getDeleteConfirmButton()).toBeDisabled() }) // Resolve and check modal closes @@ -436,14 +434,13 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert - // Assert - expect(screen.getByTestId('plugin-info-modal'))!.toBeInTheDocument() - expect(screen.getByTestId('plugin-info-modal'))!.toHaveAttribute('data-repo', 'owner/repo-name') - expect(screen.getByTestId('plugin-info-modal'))!.toHaveAttribute('data-release', '2.0.0') - expect(screen.getByTestId('plugin-info-modal'))!.toHaveAttribute('data-package', 'my-package.difypkg') + expect(screen.getByTestId('plugin-info-modal')).toBeInTheDocument() + expect(screen.getByTestId('plugin-info-modal')).toHaveAttribute('data-repo', 'owner/repo-name') + expect(screen.getByTestId('plugin-info-modal')).toHaveAttribute('data-release', '2.0.0') + expect(screen.getByTestId('plugin-info-modal')).toHaveAttribute('data-package', 'my-package.difypkg') }) it('should hide plugin info modal when close is clicked', () => { @@ -456,42 +453,11 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) - expect(screen.getByTestId('plugin-info-modal'))!.toBeInTheDocument() + fireEvent.click(getActionButtons()[0]) + expect(screen.getByTestId('plugin-info-modal')).toBeInTheDocument() fireEvent.click(screen.getByTestId('close-plugin-info')) - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert // Assert expect(screen.queryByTestId('plugin-info-modal')).not.toBeInTheDocument() }) @@ -515,7 +481,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert await waitFor(() => { @@ -541,7 +507,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert await waitFor(() => { @@ -560,7 +526,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert await waitFor(() => { @@ -584,7 +550,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert - toast is called with the translated payload await waitFor(() => { @@ -615,7 +581,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert await waitFor(() => { @@ -655,7 +621,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Wait for modal to be called await waitFor(() => { @@ -663,7 +629,7 @@ describe('Action Component', () => { }) // Invoke the callback - const call = mockSetShowUpdatePluginModal.mock.calls[0]![0] + const call = mockSetShowUpdatePluginModal.mock.calls[0][0] call.onSaveCallback() // Assert @@ -687,7 +653,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert await waitFor(() => { @@ -712,7 +678,7 @@ describe('Action Component', () => { // Act - First render and delete const { rerender } = render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { @@ -722,7 +688,7 @@ describe('Action Component', () => { // Re-render with same props mockUninstallPlugin.mockClear() rerender() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { @@ -748,7 +714,7 @@ describe('Action Component', () => { // Act const { rerender } = render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { @@ -757,7 +723,7 @@ describe('Action Component', () => { mockUninstallPlugin.mockClear() rerender() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { @@ -785,7 +751,7 @@ describe('Action Component', () => { // Act const { rerender } = render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { @@ -794,7 +760,7 @@ describe('Action Component', () => { expect(onDelete2).not.toHaveBeenCalled() rerender() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { @@ -836,7 +802,7 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert - Should use author and pluginName as fallback await waitFor(() => { @@ -860,12 +826,11 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) fireEvent.click(getDeleteConfirmButton()) // The confirm button should be disabled during deletion - // The confirm button should be disabled during deletion - expect(getDeleteConfirmButton())!.toBeDisabled() + expect(getDeleteConfirmButton()).toBeDisabled() // Resolve the deletion resolveFirst!({ success: true }) @@ -886,11 +851,10 @@ describe('Action Component', () => { // Act render() - fireEvent.click(getActionButtons()[0]!) + fireEvent.click(getActionButtons()[0]) // Assert - // Assert - expect(screen.getByText('plugin-with-special@chars#123'))!.toBeInTheDocument() + expect(screen.getByText('plugin-with-special@chars#123')).toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx index ab3382ff75..8c33894929 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx @@ -83,8 +83,8 @@ describe('usePluginTaskStatus Hook', () => { render() - expect(screen.getByTestId('running-count'))!.toHaveTextContent('1') - expect(screen.getByTestId('running-id'))!.toHaveTextContent(runningPlugin.plugin_unique_identifier) + expect(screen.getByTestId('running-count')).toHaveTextContent('1') + expect(screen.getByTestId('running-id')).toHaveTextContent(runningPlugin.plugin_unique_identifier) }) it('should categorize success plugins correctly', () => { @@ -103,8 +103,8 @@ describe('usePluginTaskStatus Hook', () => { render() - expect(screen.getByTestId('success-count'))!.toHaveTextContent('1') - expect(screen.getByTestId('success-id'))!.toHaveTextContent(successPlugin.plugin_unique_identifier) + expect(screen.getByTestId('success-count')).toHaveTextContent('1') + expect(screen.getByTestId('success-id')).toHaveTextContent(successPlugin.plugin_unique_identifier) }) it('should categorize error plugins correctly', () => { @@ -123,8 +123,8 @@ describe('usePluginTaskStatus Hook', () => { render() - expect(screen.getByTestId('error-count'))!.toHaveTextContent('1') - expect(screen.getByTestId('error-id'))!.toHaveTextContent(errorPlugin.plugin_unique_identifier) + expect(screen.getByTestId('error-count')).toHaveTextContent('1') + expect(screen.getByTestId('error-id')).toHaveTextContent(errorPlugin.plugin_unique_identifier) }) it('should categorize mixed plugins correctly', () => { @@ -149,10 +149,10 @@ describe('usePluginTaskStatus Hook', () => { render() - expect(screen.getByTestId('running'))!.toHaveTextContent('1') - expect(screen.getByTestId('success'))!.toHaveTextContent('1') - expect(screen.getByTestId('error'))!.toHaveTextContent('1') - expect(screen.getByTestId('total'))!.toHaveTextContent('3') + expect(screen.getByTestId('running')).toHaveTextContent('1') + expect(screen.getByTestId('success')).toHaveTextContent('1') + expect(screen.getByTestId('error')).toHaveTextContent('1') + expect(screen.getByTestId('total')).toHaveTextContent('3') }) }) @@ -175,11 +175,11 @@ describe('usePluginTaskStatus Hook', () => { render() - expect(screen.getByTestId('isInstalling'))!.toHaveTextContent('true') - expect(screen.getByTestId('isInstallingWithSuccess'))!.toHaveTextContent('false') - expect(screen.getByTestId('isInstallingWithError'))!.toHaveTextContent('false') - expect(screen.getByTestId('isSuccess'))!.toHaveTextContent('false') - expect(screen.getByTestId('isFailed'))!.toHaveTextContent('false') + expect(screen.getByTestId('isInstalling')).toHaveTextContent('true') + expect(screen.getByTestId('isInstallingWithSuccess')).toHaveTextContent('false') + expect(screen.getByTestId('isInstallingWithError')).toHaveTextContent('false') + expect(screen.getByTestId('isSuccess')).toHaveTextContent('false') + expect(screen.getByTestId('isFailed')).toHaveTextContent('false') }) it('should set isInstallingWithSuccess when running and success plugins exist', () => { @@ -194,7 +194,7 @@ describe('usePluginTaskStatus Hook', () => { } render() - expect(screen.getByTestId('flag'))!.toHaveTextContent('true') + expect(screen.getByTestId('flag')).toHaveTextContent('true') }) it('should set isInstallingWithError when running and error plugins exist', () => { @@ -209,7 +209,7 @@ describe('usePluginTaskStatus Hook', () => { } render() - expect(screen.getByTestId('flag'))!.toHaveTextContent('true') + expect(screen.getByTestId('flag')).toHaveTextContent('true') }) it('should set isSuccess when all plugins succeeded', () => { @@ -224,7 +224,7 @@ describe('usePluginTaskStatus Hook', () => { } render() - expect(screen.getByTestId('flag'))!.toHaveTextContent('true') + expect(screen.getByTestId('flag')).toHaveTextContent('true') }) it('should set isFailed when no running plugins and some failed', () => { @@ -239,7 +239,7 @@ describe('usePluginTaskStatus Hook', () => { } render() - expect(screen.getByTestId('flag'))!.toHaveTextContent('true') + expect(screen.getByTestId('flag')).toHaveTextContent('true') }) }) @@ -296,12 +296,12 @@ describe('TaskStatusIndicator Component', () => { describe('Rendering', () => { it('should render without crashing', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should render with correct id', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) }) @@ -309,18 +309,17 @@ describe('TaskStatusIndicator Component', () => { it('should show downloading icon when installing', () => { render() // DownloadingIcon is rendered when isInstalling is true - // DownloadingIcon is rendered when isInstalling is true - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show downloading icon when installing with error', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show install icon when not installing', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) }) @@ -334,7 +333,7 @@ describe('TaskStatusIndicator Component', () => { totalPluginsLength={3} />, ) - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show progress circle when installing with success', () => { @@ -346,7 +345,7 @@ describe('TaskStatusIndicator Component', () => { totalPluginsLength={3} />, ) - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show error progress circle when installing with error', () => { @@ -358,7 +357,7 @@ describe('TaskStatusIndicator Component', () => { totalPluginsLength={3} />, ) - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show success icon when all completed successfully', () => { @@ -371,12 +370,12 @@ describe('TaskStatusIndicator Component', () => { totalPluginsLength={3} />, ) - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show error icon when failed', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) }) @@ -384,19 +383,19 @@ describe('TaskStatusIndicator Component', () => { it('should apply error styles when installing with error', () => { render() const trigger = document.getElementById('plugin-task-trigger') - expect(trigger)!.toHaveClass('bg-state-destructive-hover') + expect(trigger).toHaveClass('bg-state-destructive-hover') }) it('should apply error styles when failed', () => { render() const trigger = document.getElementById('plugin-task-trigger') - expect(trigger)!.toHaveClass('bg-state-destructive-hover') + expect(trigger).toHaveClass('bg-state-destructive-hover') }) it('should apply cursor-pointer when clickable', () => { render() const trigger = document.getElementById('plugin-task-trigger') - expect(trigger)!.toHaveClass('cursor-pointer') + expect(trigger).toHaveClass('cursor-pointer') }) }) @@ -430,7 +429,7 @@ describe('PluginTaskList Component', () => { describe('Rendering', () => { it('should render without crashing with empty lists', () => { render() - expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() + expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) it('should render running plugins section when plugins exist', () => { @@ -440,8 +439,7 @@ describe('PluginTaskList Component', () => { // Translation key is returned as text in tests, multiple matches expected (title + status) expect(screen.getAllByText(/task\.installing/i).length).toBeGreaterThan(0) // Verify section container is rendered - // Verify section container is rendered - expect(document.querySelector('.max-h-\\[300px\\]'))!.toBeInTheDocument() + expect(document.querySelector('.max-h-\\[300px\\]')).toBeInTheDocument() }) it('should render success plugins section when plugins exist', () => { @@ -456,7 +454,7 @@ describe('PluginTaskList Component', () => { const errorPlugins = [createMockPlugin({ status: TaskStatus.failed, message: 'Error occurred' })] render() - expect(screen.getByText('Error occurred'))!.toBeInTheDocument() + expect(screen.getByText('Error occurred')).toBeInTheDocument() }) it('should render all sections when all types exist', () => { @@ -543,7 +541,7 @@ describe('PluginTaskList Component', () => { render() - expect(screen.getByText('My Test Plugin'))!.toBeInTheDocument() + expect(screen.getByText('My Test Plugin')).toBeInTheDocument() }) it('should display plugin message when available', () => { @@ -554,7 +552,7 @@ describe('PluginTaskList Component', () => { render() - expect(screen.getByText('Successfully installed!'))!.toBeInTheDocument() + expect(screen.getByText('Successfully installed!')).toBeInTheDocument() }) it('should display multiple plugins in each section', () => { @@ -565,8 +563,8 @@ describe('PluginTaskList Component', () => { render() - expect(screen.getByText('Plugin A'))!.toBeInTheDocument() - expect(screen.getByText('Plugin B'))!.toBeInTheDocument() + expect(screen.getByText('Plugin A')).toBeInTheDocument() + expect(screen.getByText('Plugin B')).toBeInTheDocument() // Count is rendered, verify multiple items are in list expect(document.querySelectorAll('.hover\\:bg-state-base-hover').length).toBe(2) }) @@ -595,7 +593,7 @@ describe('PluginTasks Component', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) }) @@ -606,8 +604,7 @@ describe('PluginTasks Component', () => { render() // The component renders with a tooltip, we verify it exists - // The component renders with a tooltip, we verify it exists - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show success tip when all succeeded', () => { @@ -615,7 +612,7 @@ describe('PluginTasks Component', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show error tip when some failed', () => { @@ -626,7 +623,7 @@ describe('PluginTasks Component', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) }) @@ -640,8 +637,7 @@ describe('PluginTasks Component', () => { fireEvent.click(document.getElementById('plugin-task-trigger')!) // The popover content should be visible (PluginTaskList) - // The popover content should be visible (PluginTaskList) - expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() + expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) it('should not toggle when status does not allow', () => { @@ -651,8 +647,7 @@ describe('PluginTasks Component', () => { render() // Component should still render - // Component should still render - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) }) @@ -670,7 +665,7 @@ describe('PluginTasks Component', () => { // Wait for popover content to render await waitFor(() => { - expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() + expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) // Find and click clear all button @@ -716,13 +711,13 @@ describe('PluginTasks Component', () => { fireEvent.click(document.getElementById('plugin-task-trigger')!) await waitFor(() => { - expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() + expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) // Find and click the clear all button in error section const clearButtons = screen.getAllByRole('button') if (clearButtons.length > 0) - fireEvent.click(clearButtons[0]!) + fireEvent.click(clearButtons[0]) await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalled() @@ -744,13 +739,13 @@ describe('PluginTasks Component', () => { fireEvent.click(document.getElementById('plugin-task-trigger')!) await waitFor(() => { - expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() + expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) // Find and click individual clear button (usually the last one) const clearButtons = screen.getAllByRole('button') const individualClearButton = clearButtons[clearButtons.length - 1] - fireEvent.click(individualClearButton!) + fireEvent.click(individualClearButton) await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith({ @@ -775,7 +770,7 @@ describe('PluginTasks Component', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should handle many plugins', () => { @@ -788,7 +783,7 @@ describe('PluginTasks Component', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should handle plugins with empty labels', () => { @@ -800,7 +795,7 @@ describe('PluginTasks Component', () => { render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should handle plugins with long messages', () => { @@ -815,30 +810,6 @@ describe('PluginTasks Component', () => { // Open popover fireEvent.click(document.getElementById('plugin-task-trigger')!) - expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() - }) - - it('should open for installing-with-success state', () => { - setupMocks([ - createMockPlugin({ status: TaskStatus.running, plugin_unique_identifier: 'running-1' }), - createMockPlugin({ status: TaskStatus.success, plugin_unique_identifier: 'success-1' }), - ]) - - render() - fireEvent.click(document.getElementById('plugin-task-trigger')!) - - expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() - }) - - it('should open for installing-with-error state', () => { - setupMocks([ - createMockPlugin({ status: TaskStatus.running, plugin_unique_identifier: 'running-1' }), - createMockPlugin({ status: TaskStatus.failed, plugin_unique_identifier: 'failed-1' }), - ]) - - render() - fireEvent.click(document.getElementById('plugin-task-trigger')!) - expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) @@ -882,13 +853,13 @@ describe('PluginTasks Integration', () => { const { rerender } = render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() // Simulate completion by re-rendering with success setupMocks([createMockPlugin({ status: TaskStatus.success })]) rerender() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should show correct UI flow from installing to failure', async () => { @@ -897,13 +868,13 @@ describe('PluginTasks Integration', () => { const { rerender } = render() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() // Simulate failure by re-rendering with failed setupMocks([createMockPlugin({ status: TaskStatus.failed, message: 'Network error' })]) rerender() - expect(document.getElementById('plugin-task-trigger'))!.toBeInTheDocument() + expect(document.getElementById('plugin-task-trigger')).toBeInTheDocument() }) it('should handle mixed status during installation', () => { diff --git a/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx b/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx index f71780e2df..26eeb7fe03 100644 --- a/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx +++ b/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx @@ -148,8 +148,7 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.title')).toBeInTheDocument() }) it('should render install permission section', () => { @@ -157,8 +156,7 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByText('plugin.privilege.whoCanInstall'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.whoCanInstall')).toBeInTheDocument() }) it('should render debug permission section', () => { @@ -166,8 +164,7 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByText('plugin.privilege.whoCanDebug'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.whoCanDebug')).toBeInTheDocument() }) it('should render all permission options for install', () => { @@ -183,9 +180,8 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByText('common.operation.cancel'))!.toBeInTheDocument() - expect(screen.getByText('common.operation.save'))!.toBeInTheDocument() + expect(screen.getByText('common.operation.cancel')).toBeInTheDocument() + expect(screen.getByText('common.operation.save')).toBeInTheDocument() }) it('should render AutoUpdateSetting when marketplace is enabled', () => { @@ -196,8 +192,7 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByTestId('auto-update-setting'))!.toBeInTheDocument() + expect(screen.getByTestId('auto-update-setting')).toBeInTheDocument() }) it('should not render AutoUpdateSetting when marketplace is disabled', () => { @@ -207,37 +202,6 @@ describe('reference-setting-modal', () => { // Act render() - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert // Assert expect(screen.queryByTestId('auto-update-setting')).not.toBeInTheDocument() }) @@ -247,8 +211,7 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByTestId('modal-close'))!.toBeInTheDocument() + expect(screen.getByTestId('modal-close')).toBeInTheDocument() }) }) @@ -267,11 +230,11 @@ describe('reference-setting-modal', () => { // Assert - admin option should be selected for install (first one) const adminOptions = screen.getAllByTestId('option-card-plugin.privilege.admins') - expect(adminOptions[0])!.toHaveAttribute('aria-pressed', 'true') // Install permission + expect(adminOptions[0]).toHaveAttribute('aria-pressed', 'true') // Install permission // Assert - noOne option should be selected for debug (second one) const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') - expect(noOneOptions[1])!.toHaveAttribute('aria-pressed', 'true') // Debug permission + expect(noOneOptions[1]).toHaveAttribute('aria-pressed', 'true') // Debug permission }) it('should update tempPrivilege when permission option is clicked', () => { @@ -280,11 +243,10 @@ describe('reference-setting-modal', () => { // Act - click on "No One" for install permission const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') - fireEvent.click(noOneOptions[0]!) // First one is for install permission + fireEvent.click(noOneOptions[0]) // First one is for install permission // Assert - the option should now be selected - // Assert - the option should now be selected - expect(noOneOptions[0])!.toHaveAttribute('aria-pressed', 'true') + expect(noOneOptions[0]).toHaveAttribute('aria-pressed', 'true') }) it('should initialize with payload auto_upgrade values', () => { @@ -299,8 +261,7 @@ describe('reference-setting-modal', () => { render() // Assert - // Assert - expect(screen.getByTestId('auto-update-strategy'))!.toHaveTextContent('latest') + expect(screen.getByTestId('auto-update-strategy')).toHaveTextContent('latest') }) it('should use default auto_upgrade when payload.auto_upgrade is undefined', () => { @@ -314,8 +275,7 @@ describe('reference-setting-modal', () => { render() // Assert - should use default value (disabled) - // Assert - should use default value (disabled) - expect(screen.getByTestId('auto-update-strategy'))!.toHaveTextContent('disabled') + expect(screen.getByTestId('auto-update-strategy')).toHaveTextContent('disabled') }) }) @@ -391,11 +351,10 @@ describe('reference-setting-modal', () => { // Click Everyone for install permission const everyoneOptions = screen.getAllByTestId('option-card-plugin.privilege.everyone') - fireEvent.click(everyoneOptions[0]!) + fireEvent.click(everyoneOptions[0]) // Assert - // Assert - expect(everyoneOptions[0])!.toHaveAttribute('aria-pressed', 'true') + expect(everyoneOptions[0]).toHaveAttribute('aria-pressed', 'true') }) it('should update debug permission when Admins Only option is clicked', () => { @@ -412,11 +371,10 @@ describe('reference-setting-modal', () => { // Click Admins Only for debug permission (second set of options) const adminOptions = screen.getAllByTestId('option-card-plugin.privilege.admins') - fireEvent.click(adminOptions[1]!) // Second one is for debug permission + fireEvent.click(adminOptions[1]) // Second one is for debug permission // Assert - // Assert - expect(adminOptions[1])!.toHaveAttribute('aria-pressed', 'true') + expect(adminOptions[1]).toHaveAttribute('aria-pressed', 'true') }) it('should update auto_upgrade config when changed in AutoUpdateSetting', async () => { @@ -452,8 +410,7 @@ describe('reference-setting-modal', () => { rerender() // Assert - component should render without issues - // Assert - component should render without issues - expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.title')).toBeInTheDocument() }) it('handleSave should be memoized with useCallback', async () => { @@ -477,11 +434,10 @@ describe('reference-setting-modal', () => { // Act - click install permission option const everyoneOptions = screen.getAllByTestId('option-card-plugin.privilege.everyone') - fireEvent.click(everyoneOptions[0]!) + fireEvent.click(everyoneOptions[0]) // Assert - install permission should be updated - // Assert - install permission should be updated - expect(everyoneOptions[0])!.toHaveAttribute('aria-pressed', 'true') + expect(everyoneOptions[0]).toHaveAttribute('aria-pressed', 'true') }) }) @@ -500,7 +456,7 @@ describe('reference-setting-modal', () => { // Act & Assert - should not crash render() - expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.title')).toBeInTheDocument() }) it('should handle undefined permission values', () => { @@ -515,7 +471,7 @@ describe('reference-setting-modal', () => { // Assert - should use default PermissionType.noOne const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') - expect(noOneOptions[0])!.toHaveAttribute('aria-pressed', 'true') + expect(noOneOptions[0]).toHaveAttribute('aria-pressed', 'true') }) it('should handle missing install_permission', () => { @@ -531,8 +487,7 @@ describe('reference-setting-modal', () => { render() // Assert - should fall back to PermissionType.noOne - // Assert - should fall back to PermissionType.noOne - expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.title')).toBeInTheDocument() }) it('should handle missing debug_permission', () => { @@ -548,8 +503,7 @@ describe('reference-setting-modal', () => { render() // Assert - should fall back to PermissionType.noOne - // Assert - should fall back to PermissionType.noOne - expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.title')).toBeInTheDocument() }) it('should handle slow async onSave gracefully', async () => { @@ -601,8 +555,7 @@ describe('reference-setting-modal', () => { const { unmount } = render() // Assert - should render without crashing - // Assert - should render without crashing - expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.title')).toBeInTheDocument() unmount() }) @@ -629,8 +582,7 @@ describe('reference-setting-modal', () => { const { unmount } = render() // Assert - // Assert - expect(screen.getByTestId('auto-update-strategy'))!.toHaveTextContent(strategy) + expect(screen.getByTestId('auto-update-strategy')).toHaveTextContent(strategy) unmount() }) @@ -656,8 +608,7 @@ describe('reference-setting-modal', () => { const { unmount } = render() // Assert - // Assert - expect(screen.getByTestId('auto-update-mode'))!.toHaveTextContent(mode) + expect(screen.getByTestId('auto-update-mode')).toHaveTextContent(mode) unmount() }) @@ -680,7 +631,7 @@ describe('reference-setting-modal', () => { // Change install permission to noOne const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') - fireEvent.click(noOneOptions[0]!) + fireEvent.click(noOneOptions[0]) // Save fireEvent.click(screen.getByText('common.operation.save')) @@ -711,7 +662,7 @@ describe('reference-setting-modal', () => { // Change debug permission to noOne const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') - fireEvent.click(noOneOptions[1]!) // Second one is for debug + fireEvent.click(noOneOptions[1]) // Second one is for debug // Save fireEvent.click(screen.getByText('common.operation.save')) @@ -740,7 +691,7 @@ describe('reference-setting-modal', () => { // Change install permission const everyoneOptions = screen.getAllByTestId('option-card-plugin.privilege.everyone') - fireEvent.click(everyoneOptions[0]!) + fireEvent.click(everyoneOptions[0]) // Save fireEvent.click(screen.getByText('common.operation.save')) @@ -766,7 +717,7 @@ describe('reference-setting-modal', () => { // Assert const modal = screen.getByTestId('modal') - expect(modal)!.toHaveClass('w-[620px]', 'max-w-[620px]', 'p-0!') + expect(modal).toHaveClass('w-[620px]', 'max-w-[620px]', 'p-0!') }) it('should pass isShow=true to Modal', () => { @@ -774,8 +725,7 @@ describe('reference-setting-modal', () => { render() // Assert - modal should be visible - // Assert - modal should be visible - expect(screen.getByTestId('modal'))!.toBeInTheDocument() + expect(screen.getByTestId('modal')).toBeInTheDocument() }) }) @@ -786,8 +736,8 @@ describe('reference-setting-modal', () => { // Assert - check order by getting all section labels const labels = screen.getAllByText(/plugin\.privilege\.whoCan/) - expect(labels[0])!.toHaveTextContent('plugin.privilege.whoCanInstall') - expect(labels[1])!.toHaveTextContent('plugin.privilege.whoCanDebug') + expect(labels[0]).toHaveTextContent('plugin.privilege.whoCanInstall') + expect(labels[1]).toHaveTextContent('plugin.privilege.whoCanDebug') }) it('should render three options per permission section', () => { @@ -812,8 +762,8 @@ describe('reference-setting-modal', () => { const cancelButton = screen.getByText('common.operation.cancel') const saveButton = screen.getByText('common.operation.save') - expect(cancelButton)!.toBeInTheDocument() - expect(saveButton)!.toBeInTheDocument() + expect(cancelButton).toBeInTheDocument() + expect(saveButton).toBeInTheDocument() }) }) }) @@ -847,11 +797,11 @@ describe('reference-setting-modal', () => { // Change install permission to Everyone const everyoneOptions = screen.getAllByTestId('option-card-plugin.privilege.everyone') - fireEvent.click(everyoneOptions[0]!) + fireEvent.click(everyoneOptions[0]) // Change debug permission to Admins Only const adminOptions = screen.getAllByTestId('option-card-plugin.privilege.admins') - fireEvent.click(adminOptions[1]!) + fireEvent.click(adminOptions[1]) // Change auto-update strategy fireEvent.click(screen.getByTestId('auto-update-change')) @@ -891,7 +841,7 @@ describe('reference-setting-modal', () => { // Make some changes const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') - fireEvent.click(noOneOptions[0]!) + fireEvent.click(noOneOptions[0]) // Cancel fireEvent.click(screen.getByText('common.operation.cancel')) @@ -913,9 +863,8 @@ describe('reference-setting-modal', () => { render() // Assert - Labels are rendered correctly - // Assert - Labels are rendered correctly - expect(screen.getByText('plugin.privilege.whoCanInstall'))!.toBeInTheDocument() - expect(screen.getByText('plugin.privilege.whoCanDebug'))!.toBeInTheDocument() + expect(screen.getByText('plugin.privilege.whoCanInstall')).toBeInTheDocument() + expect(screen.getByText('plugin.privilege.whoCanDebug')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx index 19e5d28e63..d0051c933b 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx @@ -28,7 +28,7 @@ const InputFieldForm = ({ initialData, supportFile = false, onCancel, onSubmit, if (!result.success) { const issues = result.error.issues const firstIssue = issues[0] - const errorMessage = `"${firstIssue!.path.join('.')}" ${firstIssue!.message}` + const errorMessage = `"${firstIssue.path.join('.')}" ${firstIssue.message}` toast.error(errorMessage) return errorMessage } diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx index 6a77c9b355..28b5e32b71 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx @@ -21,7 +21,7 @@ const Options = ({ initialData, configurations, schema, CustomActions, onSubmit if (!result.success) { const issues = result.error.issues const firstIssue = issues[0] - const errorMessage = `Path: ${firstIssue!.path.join('.')} Error: ${firstIssue!.message}` + const errorMessage = `Path: ${firstIssue.path.join('.')} Error: ${firstIssue.message}` toast.error(errorMessage) return errorMessage } @@ -41,7 +41,7 @@ const Options = ({ initialData, configurations, schema, CustomActions, onSubmit form.handleSubmit() }} > -
+
{configurations.map((config, index) => { const FieldComponent = BaseField({ initialData, diff --git a/web/app/components/tools/marketplace/index.tsx b/web/app/components/tools/marketplace/index.tsx index e7d6a22d42..449f0b8b4a 100644 --- a/web/app/components/tools/marketplace/index.tsx +++ b/web/app/components/tools/marketplace/index.tsx @@ -37,50 +37,50 @@ const Marketplace = ({ return ( <> -
+
{isMarketplaceArrowVisible && ( )} -
-
+
+
{t('marketplace.moreFrom', { ns: 'plugin' })}
-
+
{t('marketplace.discover', { ns: 'plugin' })} - + {t('category.models', { ns: 'plugin' })} , - + {t('category.tools', { ns: 'plugin' })} , - + {t('category.datasources', { ns: 'plugin' })} , - + {t('category.triggers', { ns: 'plugin' })} , - + {t('category.agents', { ns: 'plugin' })} , - + {t('category.extensions', { ns: 'plugin' })} {t('marketplace.and', { ns: 'plugin' })} - + {t('category.bundles', { ns: 'plugin' })} {t('operation.in', { ns: 'common' })} {t('marketplace.difyMarketplace', { ns: 'plugin' })} @@ -92,7 +92,7 @@ const Marketplace = ({
{ isLoading && page === 1 && ( -
+
) diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index 5216e9eede..584c9d211a 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -199,22 +199,22 @@ describe('MCPDetailContent', () => { describe('Rendering', () => { it('should render without crashing', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('Test MCP Server'))!.toBeInTheDocument() + expect(screen.getByText('Test MCP Server')).toBeInTheDocument() }) it('should display MCP name', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('Test MCP Server'))!.toBeInTheDocument() + expect(screen.getByText('Test MCP Server')).toBeInTheDocument() }) it('should display server identifier', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('test-mcp'))!.toBeInTheDocument() + expect(screen.getByText('test-mcp')).toBeInTheDocument() }) it('should display server URL', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('https://example.com/mcp'))!.toBeInTheDocument() + expect(screen.getByText('https://example.com/mcp')).toBeInTheDocument() }) it('should render close button', () => { @@ -227,8 +227,7 @@ describe('MCPDetailContent', () => { it('should render operation dropdown', () => { render(, { wrapper: createWrapper() }) // Operation dropdown trigger should be present - // Operation dropdown trigger should be present - expect(document.querySelector('button'))!.toBeInTheDocument() + expect(document.querySelector('button')).toBeInTheDocument() }) }) @@ -239,7 +238,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.authorize'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.authorize')).toBeInTheDocument() }) it('should show authorized button when authorized', () => { @@ -248,7 +247,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.auth.authorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.authorized')).toBeInTheDocument() }) it('should show authorization required message when not authorized', () => { @@ -257,7 +256,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.authorizingRequired'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.authorizingRequired')).toBeInTheDocument() }) it('should show authorization tip', () => { @@ -266,7 +265,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.authorizeTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.authorizeTip')).toBeInTheDocument() }) }) @@ -277,7 +276,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.toolsEmpty'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolsEmpty')).toBeInTheDocument() }) it('should show get tools button when empty', () => { @@ -286,7 +285,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.getTools'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.getTools')).toBeInTheDocument() }) }) @@ -295,7 +294,7 @@ describe('MCPDetailContent', () => { render(, { wrapper: createWrapper() }) // Icon container should be present const iconContainer = document.querySelector('[class*="rounded-xl"][class*="border"]') - expect(iconContainer)!.toBeInTheDocument() + expect(iconContainer).toBeInTheDocument() }) }) @@ -306,7 +305,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('Test MCP Server'))!.toBeInTheDocument() + expect(screen.getByText('Test MCP Server')).toBeInTheDocument() }) it('should handle long MCP name', () => { @@ -316,7 +315,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText(longName))!.toBeInTheDocument() + expect(screen.getByText(longName)).toBeInTheDocument() }) }) @@ -333,8 +332,8 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tool1'))!.toBeInTheDocument() - expect(screen.getByText('tool2'))!.toBeInTheDocument() + expect(screen.getByText('tool1')).toBeInTheDocument() + expect(screen.getByText('tool2')).toBeInTheDocument() }) it('should show single tool label when only one tool', () => { @@ -346,7 +345,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.onlyTool'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.onlyTool')).toBeInTheDocument() }) it('should show tools count when multiple tools', () => { @@ -361,7 +360,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText(/tools.mcp.toolsNum/))!.toBeInTheDocument() + expect(screen.getByText(/tools.mcp.toolsNum/)).toBeInTheDocument() }) }) @@ -376,7 +375,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.gettingTools'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.gettingTools')).toBeInTheDocument() }) it('should show updating state when updating tools', () => { @@ -389,7 +388,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.mcp.updateTools'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.updateTools')).toBeInTheDocument() }) it('should show authorizing button when authorizing', () => { @@ -465,7 +464,7 @@ describe('MCPDetailContent', () => { ) const authorizeBtn = screen.getByText('tools.mcp.authorize') - expect(authorizeBtn.closest('button'))!.toBeDisabled() + expect(authorizeBtn.closest('button')).toBeDisabled() }) }) @@ -484,7 +483,7 @@ describe('MCPDetailContent', () => { fireEvent.click(updateBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle')).toBeInTheDocument() }) }) @@ -504,7 +503,7 @@ describe('MCPDetailContent', () => { fireEvent.click(updateBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle')).toBeInTheDocument() }) // Confirm the update @@ -542,7 +541,7 @@ describe('MCPDetailContent', () => { fireEvent.click(editBtn) await waitFor(() => { - expect(screen.getByTestId('mcp-update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('mcp-update-modal')).toBeInTheDocument() }) }) @@ -554,7 +553,7 @@ describe('MCPDetailContent', () => { fireEvent.click(editBtn) await waitFor(() => { - expect(screen.getByTestId('mcp-update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('mcp-update-modal')).toBeInTheDocument() }) // Close modal @@ -575,7 +574,7 @@ describe('MCPDetailContent', () => { fireEvent.click(editBtn) await waitFor(() => { - expect(screen.getByTestId('mcp-update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('mcp-update-modal')).toBeInTheDocument() }) // Confirm form @@ -602,7 +601,7 @@ describe('MCPDetailContent', () => { fireEvent.click(editBtn) await waitFor(() => { - expect(screen.getByTestId('mcp-update-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('mcp-update-modal')).toBeInTheDocument() }) // Confirm form @@ -625,7 +624,7 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.delete'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) }) @@ -637,7 +636,7 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.delete'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Cancel @@ -657,7 +656,7 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.delete'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Confirm delete @@ -679,7 +678,7 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.delete'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Confirm delete @@ -744,7 +743,7 @@ describe('MCPDetailContent', () => { }) // Get the callback function and call it - const oauthCallback = mockOpenOAuthPopup.mock.calls[0]![1] + const oauthCallback = mockOpenOAuthPopup.mock.calls[0][1] oauthCallback() await waitFor(() => { @@ -766,7 +765,7 @@ describe('MCPDetailContent', () => { // Button should be disabled const authorizeBtn = screen.getByText('tools.mcp.authorize') - expect(authorizeBtn.closest('button'))!.toBeDisabled() + expect(authorizeBtn.closest('button')).toBeDisabled() }) }) @@ -777,7 +776,7 @@ describe('MCPDetailContent', () => { , { wrapper: createWrapper() }, ) - expect(screen.getByText('tools.auth.authorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.authorized')).toBeInTheDocument() }) it('should call handleAuthorize when authorized button is clicked', async () => { @@ -806,7 +805,7 @@ describe('MCPDetailContent', () => { ) const authorizedBtn = screen.getByText('tools.auth.authorized') - expect(authorizedBtn.closest('button'))!.toBeDisabled() + expect(authorizedBtn.closest('button')).toBeDisabled() }) }) @@ -826,7 +825,7 @@ describe('MCPDetailContent', () => { fireEvent.click(updateBtn) await waitFor(() => { - expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle'))!.toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle')).toBeInTheDocument() }) // Cancel the update diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index d510eb2a34..14ab3ebd0a 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -187,9 +187,9 @@ describe('ProviderDetail', () => { onRefreshData={mockOnRefreshData} />, ) - expect(screen.getByTestId('title'))!.toHaveTextContent('Test Collection') - expect(screen.getByTestId('org-info'))!.toHaveTextContent('Test Author') - expect(screen.getByTestId('description'))!.toHaveTextContent('A test collection') + expect(screen.getByTestId('title')).toHaveTextContent('Test Collection') + expect(screen.getByTestId('org-info')).toHaveTextContent('Test Author') + expect(screen.getByTestId('description')).toHaveTextContent('A test collection') }) it('shows loading state initially', () => { @@ -200,7 +200,7 @@ describe('ProviderDetail', () => { onRefreshData={mockOnRefreshData} />, ) - expect(screen.getByRole('status'))!.toBeInTheDocument() + expect(screen.getByRole('status')).toBeInTheDocument() }) it('renders tool list after loading for builtIn type', async () => { @@ -212,8 +212,8 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByTestId('tool-tool-1'))!.toBeInTheDocument() - expect(screen.getByTestId('tool-tool-2'))!.toBeInTheDocument() + expect(screen.getByTestId('tool-tool-1')).toBeInTheDocument() + expect(screen.getByTestId('tool-tool-2')).toBeInTheDocument() }) }) @@ -239,7 +239,7 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) }) @@ -252,7 +252,7 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.authorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.authorized')).toBeInTheDocument() }) }) }) @@ -273,7 +273,7 @@ describe('ProviderDetail', () => { expect(mockFetchCustomCollection).toHaveBeenCalledWith('test-collection') }) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) }) }) @@ -291,8 +291,8 @@ describe('ProviderDetail', () => { expect(mockFetchWorkflowToolDetail).toHaveBeenCalledWith('test-id') }) await waitFor(() => { - expect(screen.getByText('tools.openInStudio'))!.toBeInTheDocument() - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.openInStudio')).toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) }) }) @@ -315,7 +315,7 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.unauthorized')) expect(mockSetShowModelModal).toHaveBeenCalled() @@ -332,7 +332,7 @@ describe('ProviderDetail', () => { />, ) const buttons = screen.getAllByRole('button') - fireEvent.click(buttons[0]!) + fireEvent.click(buttons[0]) expect(mockOnHide).toHaveBeenCalled() }) }) @@ -388,10 +388,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.unauthorized')) - expect(screen.getByTestId('config-credential'))!.toBeInTheDocument() + expect(screen.getByTestId('config-credential')).toBeInTheDocument() }) it('saves credentials and refreshes data', async () => { @@ -403,7 +403,7 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.unauthorized')) await act(async () => { @@ -424,7 +424,7 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.unauthorized')) await act(async () => { @@ -445,10 +445,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.authorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.authorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.authorized')) - expect(screen.getByTestId('config-credential'))!.toBeInTheDocument() + expect(screen.getByTestId('config-credential')).toBeInTheDocument() }) }) @@ -467,10 +467,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.unauthorized')) - const call = mockSetShowModelModal.mock.calls[0]![0] + const call = mockSetShowModelModal.mock.calls[0][0] act(() => { call.onSaveCallback() }) @@ -497,7 +497,7 @@ describe('ProviderDetail', () => { expect(mockFetchCustomCollection).toHaveBeenCalled() }) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) }) @@ -513,10 +513,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) - expect(screen.getByTestId('edit-custom-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('edit-custom-modal')).toBeInTheDocument() await act(async () => { fireEvent.click(screen.getByTestId('edit-save')) }) @@ -538,11 +538,11 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) fireEvent.click(screen.getByTestId('edit-remove')) - expect(screen.getByText('tools.createTool.deleteToolConfirmTitle'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.deleteToolConfirmTitle')).toBeInTheDocument() await act(async () => { fireEvent.click(getDeleteConfirmButton()) }) @@ -574,10 +574,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('query'))!.toBeInTheDocument() - expect(screen.getByText('string'))!.toBeInTheDocument() - expect(screen.getByText('Search query'))!.toBeInTheDocument() - expect(screen.getByText('limit'))!.toBeInTheDocument() + expect(screen.getByText('query')).toBeInTheDocument() + expect(screen.getByText('string')).toBeInTheDocument() + expect(screen.getByText('Search query')).toBeInTheDocument() + expect(screen.getByText('limit')).toBeInTheDocument() }) }) @@ -590,10 +590,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) - expect(screen.getByTestId('workflow-tool-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('workflow-tool-modal')).toBeInTheDocument() await act(async () => { fireEvent.click(screen.getByTestId('wf-save')) }) @@ -612,11 +612,11 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) fireEvent.click(screen.getByTestId('wf-remove')) - expect(screen.getByText('tools.createTool.deleteToolConfirmTitle'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.deleteToolConfirmTitle')).toBeInTheDocument() await act(async () => { fireEvent.click(getDeleteConfirmButton()) }) @@ -637,10 +637,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.auth.unauthorized'))!.toBeInTheDocument() + expect(screen.getByText('tools.auth.unauthorized')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.auth.unauthorized')) - expect(screen.getByTestId('config-credential'))!.toBeInTheDocument() + expect(screen.getByTestId('config-credential')).toBeInTheDocument() fireEvent.click(screen.getByTestId('credential-cancel')) expect(screen.queryByTestId('config-credential')).not.toBeInTheDocument() }) @@ -657,10 +657,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) - expect(screen.getByTestId('edit-custom-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('edit-custom-modal')).toBeInTheDocument() fireEvent.click(screen.getByTestId('edit-close')) expect(screen.queryByTestId('edit-custom-modal')).not.toBeInTheDocument() }) @@ -674,10 +674,10 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) - expect(screen.getByTestId('workflow-tool-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('workflow-tool-modal')).toBeInTheDocument() fireEvent.click(screen.getByTestId('wf-close')) expect(screen.queryByTestId('workflow-tool-modal')).not.toBeInTheDocument() }) @@ -696,11 +696,11 @@ describe('ProviderDetail', () => { />, ) await waitFor(() => { - expect(screen.getByText('tools.createTool.editAction'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.editAction')).toBeInTheDocument() }) fireEvent.click(screen.getByText('tools.createTool.editAction')) fireEvent.click(screen.getByTestId('edit-remove')) - expect(screen.getByText('tools.createTool.deleteToolConfirmTitle'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.deleteToolConfirmTitle')).toBeInTheDocument() fireEvent.click(getDeleteCancelButton()) await waitFor(() => { expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() diff --git a/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx index d1126bf762..4379bec035 100644 --- a/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx @@ -26,28 +26,26 @@ describe('MethodSelector', () => { renderComponent() // Should display the current method text - // Should display the current method text - expect(screen.getByText('tools.createTool.toolInput.methodParameter'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameter')).toBeInTheDocument() }) it('should render with llm value selected', () => { renderComponent({ value: 'llm' }) - expect(screen.getByText('tools.createTool.toolInput.methodParameter'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameter')).toBeInTheDocument() }) it('should render with form value selected', () => { renderComponent({ value: 'form' }) - expect(screen.getByText('tools.createTool.toolInput.methodSetting'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) it('should render with undefined value', () => { renderComponent({ value: undefined }) // When value is undefined, it should show the form method text (else branch) - // When value is undefined, it should show the form method text (else branch) - expect(screen.getByText('tools.createTool.toolInput.methodSetting'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) it('should render arrow down icon', () => { @@ -55,7 +53,7 @@ describe('MethodSelector', () => { // The arrow icon is rendered with remixicon const arrowIcon = document.querySelector('.remixicon') - expect(arrowIcon)!.toBeInTheDocument() + expect(arrowIcon).toBeInTheDocument() }) }) @@ -64,19 +62,19 @@ describe('MethodSelector', () => { it('should display methodParameter when value is llm', () => { renderComponent({ value: 'llm' }) - expect(screen.getByText('tools.createTool.toolInput.methodParameter'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameter')).toBeInTheDocument() }) it('should display methodSetting when value is form', () => { renderComponent({ value: 'form' }) - expect(screen.getByText('tools.createTool.toolInput.methodSetting'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) it('should handle empty string value as non-llm', () => { renderComponent({ value: '' }) - expect(screen.getByText('tools.createTool.toolInput.methodSetting'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) }) @@ -92,8 +90,8 @@ describe('MethodSelector', () => { // Dropdown should now show both options with tips await waitFor(() => { - expect(screen.getByText('tools.createTool.toolInput.methodParameterTip'))!.toBeInTheDocument() - expect(screen.getByText('tools.createTool.toolInput.methodSettingTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSettingTip')).toBeInTheDocument() }) }) @@ -108,12 +106,12 @@ describe('MethodSelector', () => { // Wait for dropdown to open await waitFor(() => { - expect(screen.getByText('tools.createTool.toolInput.methodParameterTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument() }) // Click the llm option (by finding the method parameter option in dropdown) const llmOption = screen.getAllByText('tools.createTool.toolInput.methodParameter')[0] - await user.click(llmOption!) + await user.click(llmOption) expect(onChange).toHaveBeenCalledWith('llm') }) @@ -129,12 +127,12 @@ describe('MethodSelector', () => { // Wait for dropdown to open await waitFor(() => { - expect(screen.getByText('tools.createTool.toolInput.methodSettingTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSettingTip')).toBeInTheDocument() }) // Click the form option (by finding the method setting option in dropdown) const formOption = screen.getAllByText('tools.createTool.toolInput.methodSetting')[0] - await user.click(formOption!) + await user.click(formOption) expect(onChange).toHaveBeenCalledWith('form') }) @@ -148,7 +146,7 @@ describe('MethodSelector', () => { // First click - open await user.click(trigger) await waitFor(() => { - expect(screen.getByText('tools.createTool.toolInput.methodParameterTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument() }) // Second click - close @@ -165,7 +163,7 @@ describe('MethodSelector', () => { renderComponent() const trigger = document.querySelector('.hover\\:bg-background-section-burn') - expect(trigger)!.toBeInTheDocument() + expect(trigger).toBeInTheDocument() }) it('should apply open state styles when dropdown is open', async () => { @@ -177,7 +175,7 @@ describe('MethodSelector', () => { await waitFor(() => { const openTrigger = document.querySelector('.bg-background-section-burn\\!') - expect(openTrigger)!.toBeInTheDocument() + expect(openTrigger).toBeInTheDocument() }) }) @@ -191,7 +189,7 @@ describe('MethodSelector', () => { await waitFor(() => { // Check icon should be visible for llm option const checkIcon = document.querySelector('.text-text-accent') - expect(checkIcon)!.toBeInTheDocument() + expect(checkIcon).toBeInTheDocument() }) }) @@ -205,7 +203,7 @@ describe('MethodSelector', () => { await waitFor(() => { // Check icon should be visible for form option const checkIcon = document.querySelector('.text-text-accent') - expect(checkIcon)!.toBeInTheDocument() + expect(checkIcon).toBeInTheDocument() }) }) }) @@ -221,9 +219,8 @@ describe('MethodSelector', () => { await waitFor(() => { // Should show both option titles and descriptions - // Should show both option titles and descriptions - expect(screen.getByText('tools.createTool.toolInput.methodParameterTip'))!.toBeInTheDocument() - expect(screen.getByText('tools.createTool.toolInput.methodSettingTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodSettingTip')).toBeInTheDocument() }) }) @@ -236,9 +233,9 @@ describe('MethodSelector', () => { await waitFor(() => { const dropdown = document.querySelector('.w-\\[320px\\]') - expect(dropdown)!.toBeInTheDocument() - expect(dropdown)!.toHaveClass('rounded-lg') - expect(dropdown)!.toHaveClass('shadow-lg') + expect(dropdown).toBeInTheDocument() + expect(dropdown).toHaveClass('rounded-lg') + expect(dropdown).toHaveClass('shadow-lg') }) }) @@ -270,8 +267,7 @@ describe('MethodSelector', () => { await user.click(trigger) // Should not crash and should be in a consistent state - // Should not crash and should be in a consistent state - expect(trigger)!.toBeInTheDocument() + expect(trigger).toBeInTheDocument() }) it('should handle selecting the already selected value', async () => { @@ -283,12 +279,12 @@ describe('MethodSelector', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText('tools.createTool.toolInput.methodParameterTip'))!.toBeInTheDocument() + expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument() }) // Click the llm option in the dropdown (the one with the tip text nearby) const llmOptionContainer = screen.getByText('tools.createTool.toolInput.methodParameterTip').closest('.cursor-pointer') - expect(llmOptionContainer)!.toBeInTheDocument() + expect(llmOptionContainer).toBeInTheDocument() await user.click(llmOptionContainer!) // Should call onChange @@ -302,7 +298,7 @@ describe('MethodSelector', () => { renderComponent() const trigger = document.querySelector('.cursor-pointer') - expect(trigger)!.toBeInTheDocument() + expect(trigger).toBeInTheDocument() }) it('should have clickable dropdown options', async () => { diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.tsx index 07eb8ffe3c..8b1ce699e7 100644 --- a/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.tsx +++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.tsx @@ -30,7 +30,7 @@ const StartNodeOption: FC = ({

{title} {subtitle && ( - + {' '} {subtitle} @@ -39,7 +39,7 @@ const StartNodeOption: FC = ({

-

+

{description}

diff --git a/web/app/components/workflow/header/running-title.tsx b/web/app/components/workflow/header/running-title.tsx index e3e2ebab75..590c6cd329 100644 --- a/web/app/components/workflow/header/running-title.tsx +++ b/web/app/components/workflow/header/running-title.tsx @@ -15,7 +15,7 @@ const RunningTitle = () => { {isChatMode ? `Test Chat${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}` : `Test Run${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`} ยท - + {t('common.viewOnly', { ns: 'workflow' })}
diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index ae9cc0a4a2..6ad307325d 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -187,18 +187,18 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { let moreDataForCheckValid let usedVars: ValueSelector[] = [] - if (node!.data.type === BlockEnum.Tool) - moreDataForCheckValid = getToolCheckParams(node!.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language) + if (node.data.type === BlockEnum.Tool) + moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language) - if (node!.data.type === BlockEnum.DataSource) - moreDataForCheckValid = getDataSourceCheckParams(node!.data as DataSourceNodeType, dataSourceList || [], language) + if (node.data.type === BlockEnum.DataSource) + moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language) - if (node!.data.type === BlockEnum.TriggerPlugin) - moreDataForCheckValid = getTriggerCheckParams(node!.data as PluginTriggerNodeType, triggerPlugins, language) + if (node.data.type === BlockEnum.TriggerPlugin) + moreDataForCheckValid = getTriggerCheckParams(node.data as PluginTriggerNodeType, triggerPlugins, language) - const toolIcon = getToolIcon(node!.data) - if (node!.data.type === BlockEnum.Agent) { - const data = node!.data as AgentNodeType + const toolIcon = getToolIcon(node.data) + if (node.data.type === BlockEnum.Agent) { + const data = node.data as AgentNodeType const isReadyForCheckValid = !!strategyProviders const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name) const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name) @@ -210,13 +210,13 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { } } else { - usedVars = getNodeUsedVars(node!).filter(v => v.length > 0) + usedVars = getNodeUsedVars(node).filter(v => v.length > 0) } - if (node!.type === CUSTOM_NODE) { - const checkData = getCheckData(node!.data) - const validator = nodesExtraData?.[node!.data.type as BlockEnum]?.checkValid - const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList }) + if (node.type === CUSTOM_NODE) { + const checkData = getCheckData(node.data) + const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid + const isPluginMissing = isNodePluginMissing(node.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList }) const errorMessages: string[] = [] @@ -224,8 +224,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { errorMessages.push(t('nodes.common.pluginNotInstalled', { ns: 'workflow' })) } else { - if (node!.data.type === BlockEnum.LLM) { - const modelProvider = (node!.data as CommonNodeType<{ model?: ModelConfig }>).model?.provider + if (node.data.type === BlockEnum.LLM) { + const modelProvider = (node.data as CommonNodeType<{ model?: ModelConfig }>).model?.provider const modelIssue = getLLMModelIssue({ modelProvider, isModelProviderInstalled: isLLMModelProviderInstalled(modelProvider, installedPluginIds), @@ -240,12 +240,12 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { errorMessages.push(validationError) } - const availableVars = map[node!.id]!.availableVars + const availableVars = map[node.id].availableVars let hasInvalidVar = false for (const variable of usedVars) { if (hasInvalidVar) break - if (isSpecialVar(variable[0]!)) + if (isSpecialVar(variable[0])) continue const usedNode = availableVars.find(v => v.nodeId === variable?.[0]) if (!usedNode || !usedNode.vars.some(v => v.variable === variable?.[1])) @@ -255,17 +255,17 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { errorMessages.push(t('errorMsg.invalidVariable', { ns: 'workflow' })) } - const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false + const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true - const isUnconnected = !validNodes.some(n => n.id === node!.id) + const isUnconnected = !validNodes.some(n => n.id === node.id) const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck) if (shouldShowError) { list.push({ - id: node!.id, - type: node!.data.type, - title: node!.data.title, + id: node.id, + type: node.data.type, + title: node.data.title, toolIcon, unConnected: isUnconnected && !canSkipConnectionCheck, errorMessages, @@ -273,7 +273,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { disableGoTo: isPluginMissing, isPluginMissing, pluginUniqueIdentifier: isPluginMissing - ? (node!.data as { plugin_unique_identifier?: string }).plugin_unique_identifier + ? (node.data as { plugin_unique_identifier?: string }).plugin_unique_identifier : undefined, }) } @@ -458,14 +458,14 @@ export const useChecklistBeforePublish = () => { const node = filteredNodes[i] let moreDataForCheckValid let usedVars: ValueSelector[] = [] - if (node!.data.type === BlockEnum.Tool) - moreDataForCheckValid = getToolCheckParams(node!.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language) + if (node.data.type === BlockEnum.Tool) + moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language) - if (node!.data.type === BlockEnum.DataSource) - moreDataForCheckValid = getDataSourceCheckParams(node!.data as DataSourceNodeType, dataSourceList || [], language) + if (node.data.type === BlockEnum.DataSource) + moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language) - if (node!.data.type === BlockEnum.Agent) { - const data = node!.data as AgentNodeType + if (node.data.type === BlockEnum.Agent) { + const data = node.data as AgentNodeType const isReadyForCheckValid = !!strategyProviders const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name) const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name) @@ -477,55 +477,55 @@ export const useChecklistBeforePublish = () => { } } else { - usedVars = getNodeUsedVars(node!).filter(v => v.length > 0) + usedVars = getNodeUsedVars(node).filter(v => v.length > 0) } - if (node!.data.type === BlockEnum.LLM) { - const modelProvider = (node!.data as CommonNodeType<{ model?: ModelConfig }>).model?.provider + if (node.data.type === BlockEnum.LLM) { + const modelProvider = (node.data as CommonNodeType<{ model?: ModelConfig }>).model?.provider const modelIssue = getLLMModelIssue({ modelProvider, isModelProviderInstalled: isLLMModelProviderInstalled(modelProvider, installedPluginIds), }) if (modelIssue === LLMModelIssueCode.providerPluginUnavailable) { - toast.error(`[${node!.data.title}] ${t('errorMsg.configureModel', { ns: 'workflow' })}`) + toast.error(`[${node.data.title}] ${t('errorMsg.configureModel', { ns: 'workflow' })}`) return false } } - const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap) - const { errorMessage } = nodesExtraData![node!.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid) + const checkData = getCheckData(node.data, datasets, embeddingProviderModelMap) + const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid) if (errorMessage) { - toast.error(`[${node!.data.title}] ${errorMessage}`) + toast.error(`[${node.data.title}] ${errorMessage}`) return false } - const availableVars = map[node!.id]!.availableVars + const availableVars = map[node.id].availableVars for (const variable of usedVars) { - const isSpecialVars = isSpecialVar(variable[0]!) + const isSpecialVars = isSpecialVar(variable[0]) if (!isSpecialVars) { const usedNode = availableVars.find(v => v.nodeId === variable?.[0]) if (usedNode) { const usedVar = usedNode.vars.find(v => v.variable === variable?.[1]) if (!usedVar) { - toast.error(`[${node!.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}`) + toast.error(`[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}`) return false } } else { - toast.error(`[${node!.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}`) + toast.error(`[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}`) return false } } } - const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false + const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true - const isUnconnected = !validNodes.some(n => n.id === node!.id) + const isUnconnected = !validNodes.some(n => n.id === node.id) if (isUnconnected && !canSkipConnectionCheck) { - toast.error(`[${node!.data.title}] ${t('common.needConnectTip', { ns: 'workflow' })}`) + toast.error(`[${node.data.title}] ${t('common.needConnectTip', { ns: 'workflow' })}`) return false } } diff --git a/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx b/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx index 3de713c067..1cd18f2907 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx @@ -53,7 +53,7 @@ const TextEditor: FC = ({ onChange={e => onChange(e.target.value)} onFocus={setIsFocus} onBlur={handleBlur} - className="h-full w-full resize-none border-none bg-transparent px-3 text-[13px] leading-[18px] font-normal text-gray-900 placeholder:text-gray-300 focus:outline-hidden" + className="h-full w-full resize-none border-none bg-transparent px-3 text-[13px] font-normal leading-[18px] text-gray-900 placeholder:text-gray-300 focus:outline-hidden" placeholder={placeholder} readOnly={readonly} /> diff --git a/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx b/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx index 4af622bb46..9884e85657 100644 --- a/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx +++ b/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx @@ -27,9 +27,9 @@ const Placeholder = () => { >
{t('nodes.tool.insertPlaceholder1', { ns: 'workflow' })} -
/
+
/
{ e.preventDefault() e.stopPropagation() diff --git a/web/app/components/workflow/nodes/_base/components/title-description-input.tsx b/web/app/components/workflow/nodes/_base/components/title-description-input.tsx index d393896bec..db34c35c91 100644 --- a/web/app/components/workflow/nodes/_base/components/title-description-input.tsx +++ b/web/app/components/workflow/nodes/_base/components/title-description-input.tsx @@ -53,7 +53,7 @@ export const TitleInput = memo(({ onChange={handleChange} onKeyDown={handleKeyDown} className={` - mr-2 h-7 min-w-0 grow appearance-none rounded-md border border-transparent bg-transparent px-1 system-xl-semibold text-text-primary + system-xl-semibold mr-2 h-7 min-w-0 grow appearance-none rounded-md border border-transparent bg-transparent px-1 text-text-primary outline-hidden focus:shadow-xs `} placeholder={t('common.addTitle', { ns: 'workflow' }) || ''} @@ -83,8 +83,8 @@ export const DescriptionInput = memo(({ return (
diff --git a/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx b/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx index 41be240bda..c5ff2ba98b 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx @@ -57,7 +57,7 @@ const ConstantField: FC = ({ {schema.type === FormTypeEnum.textNumber && ( = ({ const list = outputKeyOrders.map((key) => { return { variable: key, - variable_type: outputs[key]?.type!, + variable_type: outputs[key]?.type, } }) @@ -50,15 +50,15 @@ const OutputVarList: FC = ({ const handleVarNameChange = useCallback((index: number) => { return (e: React.ChangeEvent) => { - const oldKey = list[index]!.variable + const oldKey = list[index].variable replaceSpaceWithUnderscoreInVarNameInput(e.target) const newKey = e.target.value - validateVarInput(list.filter((_, itemIndex) => itemIndex !== index), newKey) + validateVarInput(list.toSpliced(index, 1), newKey) const newOutputs = produce(outputs, (draft) => { - draft[newKey] = draft[oldKey]! + draft[newKey] = draft[oldKey] // Only delete old key if no other entry shares this name if (!list.some((item, i) => i !== index && item.variable === oldKey)) delete draft[oldKey] @@ -69,9 +69,9 @@ const OutputVarList: FC = ({ const handleVarTypeChange = useCallback((index: number) => { return (value: string) => { - const key = list[index]!.variable + const key = list[index].variable const newOutputs = produce(outputs, (draft) => { - draft[key]!.type = value as VarType + draft[key].type = value as VarType }) onChange(newOutputs) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index 3b1aafcdf4..7fb90734c1 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -67,11 +67,11 @@ const VarList: FC = ({ const newKey = e.target.value - validateVarInput(list.filter((_, itemIndex) => itemIndex !== index), newKey) + validateVarInput(list.toSpliced(index, 1), newKey) - onVarNameChange?.(list[index]!.variable, newKey) + onVarNameChange?.(list[index].variable, newKey) const newList = produce(list, (draft) => { - draft[index]!.variable = newKey + draft[index].variable = newKey }) onChange(newList) } @@ -81,26 +81,26 @@ const VarList: FC = ({ return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => { const newList = produce(list, (draft) => { if (!isSupportConstantValue || varKindType === VarKindType.variable) { - draft[index]!.value_selector = value as ValueSelector - draft[index]!.value_type = varInfo?.type + draft[index].value_selector = value as ValueSelector + draft[index].value_type = varInfo?.type if (isSupportConstantValue) - draft[index]!.variable_type = VarKindType.variable + draft[index].variable_type = VarKindType.variable - if (!draft[index]!.variable) { + if (!draft[index].variable) { const variables = draft.map(v => v.variable) - let newVarName = value[value.length - 1]! + let newVarName = value[value.length - 1] let count = 1 - while (variables.includes(newVarName!)) { + while (variables.includes(newVarName)) { newVarName = `${value[value.length - 1]}_${count}` count++ } - draft[index]!.variable = newVarName + draft[index].variable = newVarName } } else { - draft[index]!.variable_type = VarKindType.constant - draft[index]!.value_selector = value as ValueSelector - draft[index]!.value = value as string + draft[index].variable_type = VarKindType.constant + draft[index].value_selector = value as ValueSelector + draft[index].value = value as string } }) onChange(newList) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts index 188084345e..a77af2daef 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts @@ -51,7 +51,7 @@ function useOutputVarList({ } = useDebounceFn( (id: string, newName: string) => { const oldName = oldNameRecord.current[id] - renameInspectVarName(id, oldName!, newName) + renameInspectVarName(id, oldName, newName) delete oldNameRecord.current[id] }, { wait: 500 }, @@ -73,9 +73,9 @@ function useOutputVarList({ } if (newKey) { - handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]!], [id, newKey]) + handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey]) if (!(id in oldNameRecord.current)) - oldNameRecord.current[id] = outputKeyOrders[changedIndex!]! + oldNameRecord.current[id] = outputKeyOrders[changedIndex!] renameInspectNameWithDebounce(id, newKey) } else if (changedIndex === undefined) { @@ -126,7 +126,7 @@ function useOutputVarList({ hideRemoveVarConfirm() }, [deleteInspectVar, hideRemoveVarConfirm, id, nodesWithInspectVars, removeUsedVarInNodes, removedVar]) const handleRemoveVariable = useCallback((index: number) => { - const key = outputKeyOrders[index]! + const key = outputKeyOrders[index] if (isVarUsedInNodes([id, key])) { showRemoveVarConfirm() @@ -137,15 +137,15 @@ function useOutputVarList({ const newOutputKeyOrders = outputKeyOrders.filter((_, i) => i !== index) const newInputs = produce(inputs, (draft: any) => { // Only delete from outputs when no remaining entry shares this name - if (!newOutputKeyOrders.includes(key!)) - delete draft[varKey][key!] + if (!newOutputKeyOrders.includes(key)) + delete draft[varKey][key] if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs') draft.default_value = getDefaultValue(draft as any) }) setInputs(newInputs) onOutputKeyOrdersChange(newOutputKeyOrders) - if (!newOutputKeyOrders.includes(key!)) { + if (!newOutputKeyOrders.includes(key)) { const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { return varItem.name === key })?.id diff --git a/web/app/components/workflow/nodes/http/node.tsx b/web/app/components/workflow/nodes/http/node.tsx index 97c88da18d..e40d6e2b6a 100644 --- a/web/app/components/workflow/nodes/http/node.tsx +++ b/web/app/components/workflow/nodes/http/node.tsx @@ -15,8 +15,8 @@ const Node: FC> = ({ return (
-
{method}
-
+
{method}
+
{ const { t } = useTranslation() return ( -
+
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-number.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-number.tsx index 8505fe76f9..5fec9ad5b6 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-number.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-number.tsx @@ -40,12 +40,12 @@ const ConditionNumber = ({ }, [onChange]) return ( -
+
-
+
{ valueMethod === 'variable' && !isCommonVariable && ( +
-
+
{ valueMethod === 'variable' && !isCommonVariable && ( { const { t } = useTranslation() return ( -
+
diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx index cc254d324e..859be28614 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx @@ -30,15 +30,15 @@ const Item: FC = ({
{payload.name}
-
{payload.type}
+
{payload.type}
{payload.required && ( -
{t(`${i18nPrefix}.addExtractParameterContent.required`, { ns: 'workflow' })}
+
{t(`${i18nPrefix}.addExtractParameterContent.required`, { ns: 'workflow' })}
)}
-
{payload.description}
+
{payload.description}
> = ({ return (
-
+
{/* Webhook URL Section */}
@@ -138,7 +138,7 @@ const Panel: FC> = ({
{isPrivateOrLocalAddress(inputs.webhook_debug_url) && ( -
+
{t(`${i18nPrefix}.debugUrlPrivateAddressWarning`, { ns: 'workflow' })}
)} @@ -197,7 +197,7 @@ const Panel: FC> = ({
-
-