diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh index a958832aa9..a26fd076ed 100755 --- a/.devcontainer/post_create_command.sh +++ b/.devcontainer/post_create_command.sh @@ -6,11 +6,10 @@ cd web && pnpm install pipx install uv echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc -echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin,workflow_storage\"" >> ~/.bashrc +echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor\"" >> ~/.bashrc echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev\"" >> ~/.bashrc echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc echo "alias stop-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down\"" >> ~/.bashrc source /home/vscode/.bashrc - diff --git a/api/core/mcp/client/streamable_client.py b/api/core/mcp/client/streamable_client.py index a9c1eda624..805c16c838 100644 --- a/api/core/mcp/client/streamable_client.py +++ b/api/core/mcp/client/streamable_client.py @@ -138,6 +138,10 @@ class StreamableHTTPTransport: ) -> bool: """Handle an SSE event, returning True if the response is complete.""" if sse.event == "message": + # ping event send by server will be recognized as a message event with empty data by httpx-sse's SSEDecoder + if not sse.data.strip(): + return False + try: message = JSONRPCMessage.model_validate_json(sse.data) logger.debug("SSE message: %s", message) diff --git a/api/core/rag/pipeline/queue.py b/api/core/rag/pipeline/queue.py index 3d4d6f588d..7472598a7f 100644 --- a/api/core/rag/pipeline/queue.py +++ b/api/core/rag/pipeline/queue.py @@ -54,6 +54,9 @@ class TenantIsolatedTaskQueue: serialized_data = wrapper.serialize() serialized_tasks.append(serialized_data) + if not serialized_tasks: + return + redis_client.lpush(self._queue, *serialized_tasks) def pull_tasks(self, count: int = 1) -> Sequence[Any]: diff --git a/api/models/dataset.py b/api/models/dataset.py index 01c17fa243..33d396aeb9 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -21,6 +21,7 @@ from configs import dify_config from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource from core.rag.retrieval.retrieval_methods import RetrievalMethod from extensions.ext_storage import storage +from models.base import TypeBase from services.entities.knowledge_entities.knowledge_entities import ParentMode, Rule from .account import Account @@ -906,17 +907,21 @@ class ChildChunk(Base): return db.session.query(DocumentSegment).where(DocumentSegment.id == self.segment_id).first() -class AppDatasetJoin(Base): +class AppDatasetJoin(TypeBase): __tablename__ = "app_dataset_joins" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="app_dataset_join_pkey"), sa.Index("app_dataset_join_app_dataset_idx", "dataset_id", "app_id"), ) - id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=sa.text("uuid_generate_v4()")) - app_id = mapped_column(StringUUID, nullable=False) - dataset_id = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=sa.func.current_timestamp()) + id: Mapped[str] = mapped_column( + StringUUID, primary_key=True, nullable=False, server_default=sa.text("uuid_generate_v4()"), init=False + ) + app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.func.current_timestamp(), init=False + ) @property def app(self): diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index e70b2b5c95..067feb994f 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -1,5 +1,5 @@ import json -from typing import Any +from typing import Any, TypedDict from core.app.app_config.entities import ( DatasetEntity, @@ -28,6 +28,12 @@ from models.model import App, AppMode, AppModelConfig from models.workflow import Workflow, WorkflowType +class _NodeType(TypedDict): + id: str + position: None + data: dict[str, Any] + + class WorkflowConverter: """ App Convert to Workflow Mode @@ -217,7 +223,7 @@ class WorkflowConverter: return app_config - def _convert_to_start_node(self, variables: list[VariableEntity]): + def _convert_to_start_node(self, variables: list[VariableEntity]) -> _NodeType: """ Convert to Start Node :param variables: list of variables @@ -235,7 +241,7 @@ class WorkflowConverter: def _convert_to_http_request_node( self, app_model: App, variables: list[VariableEntity], external_data_variables: list[ExternalDataVariableEntity] - ) -> tuple[list[dict], dict[str, str]]: + ) -> tuple[list[_NodeType], dict[str, str]]: """ Convert API Based Extension to HTTP Request Node :param app_model: App instance @@ -285,7 +291,7 @@ class WorkflowConverter: request_body_json = json.dumps(request_body) request_body_json = request_body_json.replace(r"\{\{", "{{").replace(r"\}\}", "}}") - http_request_node = { + http_request_node: _NodeType = { "id": f"http_request_{index}", "position": None, "data": { @@ -303,7 +309,7 @@ class WorkflowConverter: nodes.append(http_request_node) # append code node for response body parsing - code_node: dict[str, Any] = { + code_node: _NodeType = { "id": f"code_{index}", "position": None, "data": { @@ -326,7 +332,7 @@ class WorkflowConverter: def _convert_to_knowledge_retrieval_node( self, new_app_mode: AppMode, dataset_config: DatasetEntity, model_config: ModelConfigEntity - ) -> dict | None: + ) -> _NodeType | None: """ Convert datasets to Knowledge Retrieval Node :param new_app_mode: new app mode @@ -384,7 +390,7 @@ class WorkflowConverter: prompt_template: PromptTemplateEntity, file_upload: FileUploadConfig | None = None, external_data_variable_node_mapping: dict[str, str] | None = None, - ): + ) -> _NodeType: """ Convert to LLM Node :param original_app_mode: original app mode @@ -561,7 +567,7 @@ class WorkflowConverter: return template - def _convert_to_end_node(self): + def _convert_to_end_node(self) -> _NodeType: """ Convert to End Node :return: @@ -577,7 +583,7 @@ class WorkflowConverter: }, } - def _convert_to_answer_node(self): + def _convert_to_answer_node(self) -> _NodeType: """ Convert to Answer Node :return: @@ -598,7 +604,7 @@ class WorkflowConverter: """ return {"id": f"{source}-{target}", "source": source, "target": target} - def _append_node(self, graph: dict, node: dict): + def _append_node(self, graph: dict[str, Any], node: _NodeType): """ Append Node to Graph diff --git a/api/tests/unit_tests/core/rag/pipeline/test_queue.py b/api/tests/unit_tests/core/rag/pipeline/test_queue.py index cfdbb30f8f..17c5f3c6b7 100644 --- a/api/tests/unit_tests/core/rag/pipeline/test_queue.py +++ b/api/tests/unit_tests/core/rag/pipeline/test_queue.py @@ -179,7 +179,7 @@ class TestTenantIsolatedTaskQueue: """Test pushing empty task list.""" sample_queue.push_tasks([]) - mock_redis.lpush.assert_called_once_with("tenant_self_test-key_task_queue:tenant-123") + mock_redis.lpush.assert_not_called() @patch("core.rag.pipeline.queue.redis_client") def test_pull_tasks_default_count(self, mock_redis, sample_queue): diff --git a/api/tests/unit_tests/services/workflow/test_workflow_converter.py b/api/tests/unit_tests/services/workflow/test_workflow_converter.py index 63ce4c0c3c..8ea5754363 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_converter.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_converter.py @@ -199,6 +199,7 @@ def test__convert_to_knowledge_retrieval_node_for_chatbot(): node = WorkflowConverter()._convert_to_knowledge_retrieval_node( new_app_mode=new_app_mode, dataset_config=dataset_config, model_config=model_config ) + assert node is not None assert node["data"]["type"] == "knowledge-retrieval" assert node["data"]["query_variable_selector"] == ["sys", "query"] @@ -231,6 +232,7 @@ def test__convert_to_knowledge_retrieval_node_for_workflow_app(): node = WorkflowConverter()._convert_to_knowledge_retrieval_node( new_app_mode=new_app_mode, dataset_config=dataset_config, model_config=model_config ) + assert node is not None assert node["data"]["type"] == "knowledge-retrieval" assert node["data"]["query_variable_selector"] == ["start", dataset_config.retrieve_config.query_variable] diff --git a/web/app/components/base/icons/assets/vender/line/others/apps-02.svg b/web/app/components/base/icons/assets/vender/line/others/apps-02.svg deleted file mode 100644 index 8e1fec9ecc..0000000000 --- a/web/app/components/base/icons/assets/vender/line/others/apps-02.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/app/components/base/icons/assets/vender/line/others/exchange-02.svg b/web/app/components/base/icons/assets/vender/line/others/exchange-02.svg deleted file mode 100644 index 45d2770277..0000000000 --- a/web/app/components/base/icons/assets/vender/line/others/exchange-02.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/components/base/icons/assets/vender/line/others/file-code.svg b/web/app/components/base/icons/assets/vender/line/others/file-code.svg deleted file mode 100644 index eb77033a0a..0000000000 --- a/web/app/components/base/icons/assets/vender/line/others/file-code.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/components/base/icons/src/vender/line/others/Apps02.json b/web/app/components/base/icons/src/vender/line/others/Apps02.json deleted file mode 100644 index 31378e175d..0000000000 --- a/web/app/components/base/icons/src/vender/line/others/Apps02.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "16", - "height": "16", - "viewBox": "0 0 16 16", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "g", - "attributes": { - "id": "apps-2-line" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "id": "Vector", - "d": "M4.66602 7.6665C3.00916 7.6665 1.66602 6.32336 1.66602 4.6665C1.66602 3.00965 3.00916 1.6665 4.66602 1.6665C6.32287 1.6665 7.66602 3.00965 7.66602 4.6665C7.66602 6.32336 6.32287 7.6665 4.66602 7.6665ZM4.66602 14.3332C3.00916 14.3332 1.66602 12.99 1.66602 11.3332C1.66602 9.6763 3.00916 8.33317 4.66602 8.33317C6.32287 8.33317 7.66602 9.6763 7.66602 11.3332C7.66602 12.99 6.32287 14.3332 4.66602 14.3332ZM11.3327 7.6665C9.67582 7.6665 8.33268 6.32336 8.33268 4.6665C8.33268 3.00965 9.67582 1.6665 11.3327 1.6665C12.9895 1.6665 14.3327 3.00965 14.3327 4.6665C14.3327 6.32336 12.9895 7.6665 11.3327 7.6665ZM11.3327 14.3332C9.67582 14.3332 8.33268 12.99 8.33268 11.3332C8.33268 9.6763 9.67582 8.33317 11.3327 8.33317C12.9895 8.33317 14.3327 9.6763 14.3327 11.3332C14.3327 12.99 12.9895 14.3332 11.3327 14.3332ZM4.66602 6.33317C5.58649 6.33317 6.33268 5.58698 6.33268 4.6665C6.33268 3.74603 5.58649 2.99984 4.66602 2.99984C3.74554 2.99984 2.99935 3.74603 2.99935 4.6665C2.99935 5.58698 3.74554 6.33317 4.66602 6.33317ZM4.66602 12.9998C5.58649 12.9998 6.33268 12.2536 6.33268 11.3332C6.33268 10.4127 5.58649 9.6665 4.66602 9.6665C3.74554 9.6665 2.99935 10.4127 2.99935 11.3332C2.99935 12.2536 3.74554 12.9998 4.66602 12.9998ZM11.3327 6.33317C12.2531 6.33317 12.9993 5.58698 12.9993 4.6665C12.9993 3.74603 12.2531 2.99984 11.3327 2.99984C10.4122 2.99984 9.66602 3.74603 9.66602 4.6665C9.66602 5.58698 10.4122 6.33317 11.3327 6.33317ZM11.3327 12.9998C12.2531 12.9998 12.9993 12.2536 12.9993 11.3332C12.9993 10.4127 12.2531 9.6665 11.3327 9.6665C10.4122 9.6665 9.66602 10.4127 9.66602 11.3332C9.66602 12.2536 10.4122 12.9998 11.3327 12.9998Z", - "fill": "currentColor" - }, - "children": [] - } - ] - } - ] - }, - "name": "Apps02" -} diff --git a/web/app/components/base/icons/src/vender/line/others/Apps02.tsx b/web/app/components/base/icons/src/vender/line/others/Apps02.tsx deleted file mode 100644 index 3236059d8d..0000000000 --- a/web/app/components/base/icons/src/vender/line/others/Apps02.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Apps02.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Apps02' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/others/Exchange02.json b/web/app/components/base/icons/src/vender/line/others/Exchange02.json deleted file mode 100644 index 3672d8b88b..0000000000 --- a/web/app/components/base/icons/src/vender/line/others/Exchange02.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "16", - "height": "16", - "viewBox": "0 0 16 16", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M4.66602 14.3334C3.00916 14.3334 1.66602 12.9903 1.66602 11.3334C1.66602 9.67655 3.00916 8.33342 4.66602 8.33342C6.32287 8.33342 7.66602 9.67655 7.66602 11.3334C7.66602 12.9903 6.32287 14.3334 4.66602 14.3334ZM11.3327 7.66675C9.67582 7.66675 8.33268 6.3236 8.33268 4.66675C8.33268 3.00989 9.67582 1.66675 11.3327 1.66675C12.9895 1.66675 14.3327 3.00989 14.3327 4.66675C14.3327 6.3236 12.9895 7.66675 11.3327 7.66675ZM4.66602 13.0001C5.58649 13.0001 6.33268 12.2539 6.33268 11.3334C6.33268 10.4129 5.58649 9.66675 4.66602 9.66675C3.74554 9.66675 2.99935 10.4129 2.99935 11.3334C2.99935 12.2539 3.74554 13.0001 4.66602 13.0001ZM11.3327 6.33342C12.2531 6.33342 12.9993 5.58722 12.9993 4.66675C12.9993 3.74627 12.2531 3.00008 11.3327 3.00008C10.4122 3.00008 9.66602 3.74627 9.66602 4.66675C9.66602 5.58722 10.4122 6.33342 11.3327 6.33342ZM1.99935 5.33341C1.99935 3.49247 3.49174 2.00008 5.33268 2.00008H7.33268V3.33341H5.33268C4.22812 3.33341 3.33268 4.22885 3.33268 5.33341V7.33342H1.99935V5.33341ZM13.9993 8.66675H12.666V10.6667C12.666 11.7713 11.7706 12.6667 10.666 12.6667H8.66602V14.0001H10.666C12.5069 14.0001 13.9993 12.5077 13.9993 10.6667V8.66675Z", - "fill": "currentColor" - }, - "children": [] - } - ] - }, - "name": "Exchange02" -} diff --git a/web/app/components/base/icons/src/vender/line/others/Exchange02.tsx b/web/app/components/base/icons/src/vender/line/others/Exchange02.tsx deleted file mode 100644 index 4f58de3619..0000000000 --- a/web/app/components/base/icons/src/vender/line/others/Exchange02.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Exchange02.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Exchange02' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/others/FileCode.json b/web/app/components/base/icons/src/vender/line/others/FileCode.json deleted file mode 100644 index d61af3fdb3..0000000000 --- a/web/app/components/base/icons/src/vender/line/others/FileCode.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "16", - "height": "16", - "viewBox": "0 0 16 16", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "M10 2.66659H3.33333V13.3333H12.6667V5.33325H10V2.66659ZM2 1.99445C2 1.62929 2.29833 1.33325 2.66567 1.33325H10.6667L13.9998 4.66658L14 13.9949C14 14.3659 13.7034 14.6666 13.3377 14.6666H2.66227C2.29651 14.6666 2 14.3631 2 14.0054V1.99445ZM11.7713 7.99992L9.4142 10.3569L8.4714 9.41412L9.8856 7.99992L8.4714 6.58571L9.4142 5.6429L11.7713 7.99992ZM4.22877 7.99992L6.58579 5.6429L7.5286 6.58571L6.11438 7.99992L7.5286 9.41412L6.58579 10.3569L4.22877 7.99992Z", - "fill": "currentColor" - }, - "children": [] - } - ] - }, - "name": "FileCode" -} diff --git a/web/app/components/base/icons/src/vender/line/others/FileCode.tsx b/web/app/components/base/icons/src/vender/line/others/FileCode.tsx deleted file mode 100644 index 3660aad794..0000000000 --- a/web/app/components/base/icons/src/vender/line/others/FileCode.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './FileCode.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'FileCode' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/others/index.ts b/web/app/components/base/icons/src/vender/line/others/index.ts index 2322e9d9f1..99db66b397 100644 --- a/web/app/components/base/icons/src/vender/line/others/index.ts +++ b/web/app/components/base/icons/src/vender/line/others/index.ts @@ -1,10 +1,7 @@ -export { default as Apps02 } from './Apps02' export { default as BubbleX } from './BubbleX' export { default as Colors } from './Colors' export { default as DragHandle } from './DragHandle' export { default as Env } from './Env' -export { default as Exchange02 } from './Exchange02' -export { default as FileCode } from './FileCode' export { default as GlobalVariable } from './GlobalVariable' export { default as Icon3Dots } from './Icon3Dots' export { default as LongArrowLeft } from './LongArrowLeft' diff --git a/web/app/components/tools/add-tool-modal/D.png b/web/app/components/tools/add-tool-modal/D.png deleted file mode 100644 index 70b829c821..0000000000 Binary files a/web/app/components/tools/add-tool-modal/D.png and /dev/null differ diff --git a/web/app/components/tools/add-tool-modal/category.tsx b/web/app/components/tools/add-tool-modal/category.tsx deleted file mode 100644 index c1467a0ff4..0000000000 --- a/web/app/components/tools/add-tool-modal/category.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client' -import { useRef } from 'react' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { useMount } from 'ahooks' -import cn from '@/utils/classnames' -import { Apps02 } from '@/app/components/base/icons/src/vender/line/others' -import I18n from '@/context/i18n' -import { getLanguage } from '@/i18n-config/language' -import { useStore as useLabelStore } from '@/app/components/tools/labels/store' -import { fetchLabelList } from '@/service/tools' -import { renderI18nObject } from '@/i18n-config' - -type Props = { - value: string - onSelect: (type: string) => void -} - -const Icon = ({ svgString, active }: { svgString: string; active: boolean }) => { - const svgRef = useRef(null) - const SVGParser = (svg: string) => { - if (!svg) - return null - const parser = new DOMParser() - const doc = parser.parseFromString(svg, 'image/svg+xml') - return doc.documentElement - } - useMount(() => { - const svgElement = SVGParser(svgString) - if (svgRef.current && svgElement) - svgRef.current.appendChild(svgElement) - }) - return -} - -const Category = ({ - value, - onSelect, -}: Props) => { - const { t } = useTranslation() - const { locale } = useContext(I18n) - const language = getLanguage(locale) - const labelList = useLabelStore(s => s.labelList) - const setLabelList = useLabelStore(s => s.setLabelList) - - useMount(() => { - fetchLabelList().then((res) => { - setLabelList(res) - }) - }) - - return ( -
-
{t('tools.addToolModal.category').toLocaleUpperCase()}
-
onSelect('')}> - - {t('tools.type.all')} -
- {labelList.map((label) => { - const labelText = typeof label.label === 'string' - ? label.label - : (label.label ? renderI18nObject(label.label, language) : '') - return ( -
onSelect(label.name)} - > -
- -
- {labelText} -
- ) - })} -
- ) -} -export default Category diff --git a/web/app/components/tools/add-tool-modal/empty.png b/web/app/components/tools/add-tool-modal/empty.png deleted file mode 100644 index da4007e45a..0000000000 Binary files a/web/app/components/tools/add-tool-modal/empty.png and /dev/null differ diff --git a/web/app/components/tools/add-tool-modal/index.tsx b/web/app/components/tools/add-tool-modal/index.tsx deleted file mode 100644 index 392fa02f3a..0000000000 --- a/web/app/components/tools/add-tool-modal/index.tsx +++ /dev/null @@ -1,258 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { produce } from 'immer' -import { - RiAddLine, - RiCloseLine, -} from '@remixicon/react' -import { useMount } from 'ahooks' -import type { Collection, CustomCollectionBackend, Tool } from '../types' -import type { CollectionType } from '../types' -import Type from './type' -import Category from './category' -import Tools from './tools' -import cn from '@/utils/classnames' -import { basePath } from '@/utils/var' -import I18n from '@/context/i18n' -import Drawer from '@/app/components/base/drawer' -import Button from '@/app/components/base/button' -import Loading from '@/app/components/base/loading' -import Input from '@/app/components/base/input' -import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' -import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' -import { - createCustomCollection, - fetchAllBuiltInTools, - fetchAllCustomTools, - fetchAllWorkflowTools, - removeBuiltInToolCredential, - updateBuiltInToolCredential, -} from '@/service/tools' -import type { ToolWithProvider } from '@/app/components/workflow/types' -import Toast from '@/app/components/base/toast' -import ConfigContext from '@/context/debug-configuration' -import type { ModelConfig } from '@/models/debug' - -type Props = { - onHide: () => void -} -// Add and Edit -const AddToolModal: FC = ({ - onHide, -}) => { - const { t } = useTranslation() - const { locale } = useContext(I18n) - const [currentType, setCurrentType] = useState('builtin') - const [currentCategory, setCurrentCategory] = useState('') - const [keywords, setKeywords] = useState('') - const handleKeywordsChange = (value: string) => { - setKeywords(value) - } - const isMatchingKeywords = (text: string, keywords: string) => { - return text.toLowerCase().includes(keywords.toLowerCase()) - } - const [toolList, setToolList] = useState([]) - const [listLoading, setListLoading] = useState(true) - const getAllTools = async () => { - setListLoading(true) - const buildInTools = await fetchAllBuiltInTools() - if (basePath) { - buildInTools.forEach((item) => { - if (typeof item.icon == 'string' && !item.icon.includes(basePath)) - item.icon = `${basePath}${item.icon}` - }) - } - const customTools = await fetchAllCustomTools() - const workflowTools = await fetchAllWorkflowTools() - const mergedToolList = [ - ...buildInTools, - ...customTools, - ...workflowTools.filter((toolWithProvider) => { - return !toolWithProvider.tools.some((tool) => { - return !!tool.parameters.find(item => item.name === '__image') - }) - }), - ] - setToolList(mergedToolList) - setListLoading(false) - } - const filteredList = useMemo(() => { - return toolList.filter((toolWithProvider) => { - if (currentType === 'all') - return true - else - return toolWithProvider.type === currentType - }).filter((toolWithProvider) => { - if (!currentCategory) - return true - else - return toolWithProvider.labels.includes(currentCategory) - }).filter((toolWithProvider) => { - return ( - isMatchingKeywords(toolWithProvider.name, keywords) - || toolWithProvider.tools.some((tool) => { - return Object.values(tool.label).some((label) => { - return isMatchingKeywords(label, keywords) - }) - }) - ) - }) - }, [currentType, currentCategory, toolList, keywords]) - - const { - modelConfig, - setModelConfig, - } = useContext(ConfigContext) - - const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false) - const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => { - await createCustomCollection(data) - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - setIsShowEditCustomCollectionModal(false) - getAllTools() - } - const [showSettingAuth, setShowSettingAuth] = useState(false) - const [collection, setCollection] = useState() - const toolSelectHandle = (collection: Collection, tool: Tool) => { - const parameters: Record = {} - if (tool.parameters) { - tool.parameters.forEach((item) => { - parameters[item.name] = '' - }) - } - - const nexModelConfig = produce(modelConfig, (draft: ModelConfig) => { - draft.agentConfig.tools.push({ - provider_id: collection.id || collection.name, - provider_type: collection.type as CollectionType, - provider_name: collection.name, - tool_name: tool.name, - tool_label: tool.label[locale] || tool.label[locale.replaceAll('-', '_')], - tool_parameters: parameters, - enabled: true, - }) - }) - setModelConfig(nexModelConfig) - } - const authSelectHandle = (provider: Collection) => { - setCollection(provider) - setShowSettingAuth(true) - } - const updateBuiltinAuth = async (value: Record) => { - if (!collection) - return - await updateBuiltInToolCredential(collection.name, value) - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - await getAllTools() - setShowSettingAuth(false) - } - const removeBuiltinAuth = async () => { - if (!collection) - return - await removeBuiltInToolCredential(collection.name) - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - await getAllTools() - setShowSettingAuth(false) - } - - useMount(() => { - getAllTools() - }) - - return ( - <> - -
-
-
-
{t('tools.addTool')}
-
- -
-
-
- - -
-
-
-
-
- handleKeywordsChange(e.target.value)} - onClear={() => handleKeywordsChange('')} - /> -
-
-
- -
-
- {listLoading && ( -
- -
- )} - {!listLoading && ( - - )} -
-
-
- {isShowEditCollectionToolModal && ( - setIsShowEditCustomCollectionModal(false)} - onAdd={doCreateCustomToolCollection} - /> - )} - {showSettingAuth && collection && ( - setShowSettingAuth(false)} - onSaved={updateBuiltinAuth} - onRemove={removeBuiltinAuth} - /> - )} - - - ) -} -export default React.memo(AddToolModal) diff --git a/web/app/components/tools/add-tool-modal/tools.tsx b/web/app/components/tools/add-tool-modal/tools.tsx deleted file mode 100644 index 20f7e6b0da..0000000000 --- a/web/app/components/tools/add-tool-modal/tools.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { - memo, - useCallback, -} from 'react' -import { basePath } from '@/utils/var' -import { useTranslation } from 'react-i18next' -import { - RiAddLine, -} from '@remixicon/react' -import cn from '@/utils/classnames' -import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' -import { Check } from '@/app/components/base/icons/src/vender/line/general' -import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' -import type { ToolWithProvider } from '@/app/components/workflow/types' -import { BlockEnum } from '@/app/components/workflow/types' -import BlockIcon from '@/app/components/workflow/block-icon' -import Tooltip from '@/app/components/base/tooltip' -import Button from '@/app/components/base/button' -import { useGetLanguage } from '@/context/i18n' -import { useStore as useLabelStore } from '@/app/components/tools/labels/store' -import Empty from '@/app/components/tools/add-tool-modal/empty' -import type { Tool } from '@/app/components/tools/types' -import { CollectionType } from '@/app/components/tools/types' -import type { AgentTool } from '@/types/app' -import { MAX_TOOLS_NUM } from '@/config' -import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { renderI18nObject } from '@/i18n-config' - -const resolveI18nText = (value: TypeWithI18N | string | undefined, language: string): string => { - if (!value) - return '' - return typeof value === 'string' ? value : renderI18nObject(value, language) -} - -type ToolsProps = { - showWorkflowEmpty: boolean - tools: ToolWithProvider[] - addedTools: AgentTool[] - onSelect: (provider: ToolWithProvider, tool: Tool) => void - onAuthSetup: (provider: ToolWithProvider) => void -} -const Blocks = ({ - showWorkflowEmpty, - tools, - addedTools, - onSelect, - onAuthSetup, -}: ToolsProps) => { - const { t } = useTranslation() - const language = useGetLanguage() - const labelList = useLabelStore(s => s.labelList) - const addable = addedTools.length < MAX_TOOLS_NUM - - const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => { - const list = toolWithProvider.tools - const needAuth = toolWithProvider.allow_delete && !toolWithProvider.is_team_authorization && toolWithProvider.type === CollectionType.builtIn - - return ( -
-
- {resolveI18nText(toolWithProvider.label, language)} - {t('tools.addToolModal.manageInTools')} -
- {list.map((tool) => { - const labelContent = (() => { - if (!tool.labels) - return '' - return tool.labels.map((name) => { - const label = labelList.find(item => item.name === name) - return resolveI18nText(label?.label, language) - }).filter(Boolean).join(', ') - })() - const added = !!addedTools?.find(v => v.provider_id === toolWithProvider.id && v.provider_type === toolWithProvider.type && v.tool_name === tool.name) - return ( - - -
{resolveI18nText(tool.label, language)}
-
{resolveI18nText(tool.description, language)}
- {tool.labels?.length > 0 && ( -
-
- -
{labelContent}
-
-
- )} -
- )} - > -
- -
{resolveI18nText(tool.label, language)}
- {!needAuth && added && ( -
- - {t('tools.addToolModal.added').toLocaleUpperCase()} -
- )} - {!needAuth && !added && addable && ( - - )} - {needAuth && ( - - )} -
- - ) - })} - - ) - }, [addable, language, t, labelList, addedTools, onAuthSetup, onSelect]) - - return ( -
- {!tools.length && !showWorkflowEmpty && ( -
{t('workflow.tabs.noResult')}
- )} - {!tools.length && showWorkflowEmpty && ( -
- -
- )} - {!!tools.length && tools.map(renderGroup)} -
- ) -} - -export default memo(Blocks) diff --git a/web/app/components/tools/add-tool-modal/type.tsx b/web/app/components/tools/add-tool-modal/type.tsx deleted file mode 100644 index 26e78a7525..0000000000 --- a/web/app/components/tools/add-tool-modal/type.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client' -import { useTranslation } from 'react-i18next' -import cn from '@/utils/classnames' -import { Exchange02, FileCode } from '@/app/components/base/icons/src/vender/line/others' - -type Props = { - value: string - onSelect: (type: string) => void -} - -const Types = ({ - value, - onSelect, -}: Props) => { - const { t } = useTranslation() - - return ( -
-
onSelect('builtin')}> -
- {t('tools.type.builtIn')} -
-
onSelect('api')}> - - {t('tools.type.custom')} -
-
onSelect('workflow')}> - - {t('tools.type.workflow')} -
-
- ) -} -export default Types diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index 1679b4469b..01f9f09127 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -11,7 +11,7 @@ import Input from '@/app/components/base/input' import ProviderDetail from '@/app/components/tools/provider/detail' import Empty from '@/app/components/plugins/marketplace/empty' import CustomCreateCard from '@/app/components/tools/provider/custom-create-card' -import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty' +import WorkflowToolEmpty from '@/app/components/tools/provider/empty' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' diff --git a/web/app/components/tools/add-tool-modal/empty.tsx b/web/app/components/tools/provider/empty.tsx similarity index 100% rename from web/app/components/tools/add-tool-modal/empty.tsx rename to web/app/components/tools/provider/empty.tsx diff --git a/web/app/components/workflow/block-selector/tools.tsx b/web/app/components/workflow/block-selector/tools.tsx index c62f6a67f9..66d880d994 100644 --- a/web/app/components/workflow/block-selector/tools.tsx +++ b/web/app/components/workflow/block-selector/tools.tsx @@ -4,7 +4,7 @@ import IndexBar, { groupItems } from './index-bar' import type { ToolDefaultValue, ToolValue } from './types' import type { ToolTypeEnum } from './types' import { ViewType } from './view-type-select' -import Empty from '@/app/components/tools/add-tool-modal/empty' +import Empty from '@/app/components/tools/provider/empty' import { useGetLanguage } from '@/context/i18n' import ToolListTreeView from './tool/tool-list-tree-view/list' import ToolListFlatView from './tool/tool-list-flat-view/list' diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index a2190317af..fe87bc7cda 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -65,7 +65,7 @@ const AgentNode: FC> = (props) => { }) return tools }, [currentStrategy?.parameters, inputs.agent_parameters]) - return
+ return
{inputs.agent_strategy_name ? > = ({ const isSystem = isSystemVar(variable) const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0]) return ( -
+
{t(`${i18nPrefix}.inputVar`)}
=v22.11.0" }, diff --git a/web/service/tools.ts b/web/service/tools.ts index 6a88d8d567..2897ccac12 100644 --- a/web/service/tools.ts +++ b/web/service/tools.ts @@ -8,8 +8,6 @@ import type { WorkflowToolProviderRequest, WorkflowToolProviderResponse, } from '@/app/components/tools/types' -import type { ToolWithProvider } from '@/app/components/workflow/types' -import type { Label } from '@/app/components/tools/labels/constant' import { buildProviderQuery } from './_tools_util' export const fetchCollectionList = () => { @@ -112,26 +110,6 @@ export const testAPIAvailable = (payload: any) => { }) } -export const fetchAllBuiltInTools = () => { - return get('/workspaces/current/tools/builtin') -} - -export const fetchAllCustomTools = () => { - return get('/workspaces/current/tools/api') -} - -export const fetchAllWorkflowTools = () => { - return get('/workspaces/current/tools/workflow') -} - -export const fetchAllMCPTools = () => { - return get('/workspaces/current/tools/mcp') -} - -export const fetchLabelList = () => { - return get('/workspaces/current/tool-labels') -} - export const createWorkflowToolProvider = (payload: WorkflowToolProviderRequest & { workflow_app_id: string }) => { return post('/workspaces/current/tool-provider/workflow/create', { body: { ...payload }, diff --git a/web/utils/format.spec.ts b/web/utils/format.spec.ts index 20e54fe1a4..13c58bd7e5 100644 --- a/web/utils/format.spec.ts +++ b/web/utils/format.spec.ts @@ -121,7 +121,7 @@ describe('formatNumberAbbreviated', () => { expect(formatNumberAbbreviated(1000000)).toBe('1M') expect(formatNumberAbbreviated(1500000)).toBe('1.5M') expect(formatNumberAbbreviated(2300000)).toBe('2.3M') - expect(formatNumberAbbreviated(999999999)).toBe('1000M') + expect(formatNumberAbbreviated(999999999)).toBe('1B') }) it('should format billions with B suffix', () => { @@ -145,7 +145,7 @@ describe('formatNumberAbbreviated', () => { it('should handle edge cases', () => { expect(formatNumberAbbreviated(950)).toBe('950') expect(formatNumberAbbreviated(1001)).toBe('1k') - expect(formatNumberAbbreviated(999999)).toBe('1000k') + expect(formatNumberAbbreviated(999999)).toBe('1M') }) }) diff --git a/web/utils/format.ts b/web/utils/format.ts index 7fcef3669f..fe5b1deb7d 100644 --- a/web/utils/format.ts +++ b/web/utils/format.ts @@ -130,10 +130,20 @@ export const formatNumberAbbreviated = (num: number) => { for (let i = 0; i < units.length; i++) { if (num >= units[i].value) { - const formatted = (num / units[i].value).toFixed(1) + const value = num / units[i].value + let rounded = Math.round(value * 10) / 10 + let unitIndex = i + + // If rounded value >= 1000, promote to next unit + if (rounded >= 1000 && i > 0) { + rounded = rounded / 1000 + unitIndex = i - 1 + } + + const formatted = rounded.toFixed(1) return formatted.endsWith('.0') - ? `${Number.parseInt(formatted)}${units[i].symbol}` - : `${formatted}${units[i].symbol}` + ? `${Number.parseInt(formatted)}${units[unitIndex].symbol}` + : `${formatted}${units[unitIndex].symbol}` } } }