diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 82036b4475..0b40312368 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -325,8 +325,8 @@ class DatasetInitApi(Resource): @cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_rate_limit_check("knowledge") def post(self): - # The role of the current user in the ta table must be admin, owner, or editor - if not current_user.is_editor: + # 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: raise Forbidden() parser = reqparse.RequestParser() @@ -704,8 +704,8 @@ class DocumentProcessingApi(DocumentResource): document_id = str(document_id) document = self.get_document(dataset_id, document_id) - # The role of the current user in the ta table must be admin, owner, or editor - if not current_user.is_editor: + # 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: raise Forbidden() if action == "pause": @@ -769,8 +769,8 @@ class DocumentMetadataApi(DocumentResource): 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, or editor - if not current_user.is_editor: + # 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: raise Forbidden() if doc_type is None or doc_metadata is None: diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index 4642ed3573..1b38d0776a 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -123,8 +123,8 @@ class DatasetDocumentSegmentListApi(Resource): raise NotFound("Document not found.") segment_ids = request.args.getlist("segment_id") - # The role of the current user in the ta table must be admin or owner - if not current_user.is_editor: + # 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: raise Forbidden() try: DatasetService.check_dataset_permission(dataset, current_user) @@ -151,8 +151,8 @@ class DatasetDocumentSegmentApi(Resource): raise NotFound("Document not found.") # check user's model setting DatasetService.check_dataset_model_setting(dataset) - # The role of the current user in the ta table must be admin, owner, or editor - if not current_user.is_editor: + # 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: raise Forbidden() try: @@ -206,7 +206,7 @@ class DatasetDocumentSegmentAddApi(Resource): document = DocumentService.get_document(dataset_id, document_id) if not document: raise NotFound("Document not found.") - if not current_user.is_editor: + if not current_user.is_dataset_editor: raise Forbidden() # check embedding model setting if dataset.indexing_technique == "high_quality": @@ -281,8 +281,8 @@ class DatasetDocumentSegmentUpdateApi(Resource): ).first() if not segment: raise NotFound("Segment not found.") - # The role of the current user in the ta table must be admin, owner, or editor - if not current_user.is_editor: + # 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: raise Forbidden() try: DatasetService.check_dataset_permission(dataset, current_user) @@ -325,8 +325,8 @@ class DatasetDocumentSegmentUpdateApi(Resource): ).first() if not segment: raise NotFound("Segment not found.") - # The role of the current user in the ta table must be admin or owner - if not current_user.is_editor: + # 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: raise Forbidden() try: DatasetService.check_dataset_permission(dataset, current_user) @@ -428,7 +428,7 @@ class ChildChunkAddApi(Resource): ).first() if not segment: raise NotFound("Segment not found.") - if not current_user.is_editor: + if not current_user.is_dataset_editor: raise Forbidden() # check embedding model setting if dataset.indexing_technique == "high_quality": @@ -528,8 +528,8 @@ class ChildChunkAddApi(Resource): ).first() if not segment: raise NotFound("Segment not found.") - # The role of the current user in the ta table must be admin, owner, or editor - if not current_user.is_editor: + # 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: raise Forbidden() try: DatasetService.check_dataset_permission(dataset, current_user) @@ -579,8 +579,8 @@ class ChildChunkUpdateApi(Resource): ).first() if not child_chunk: raise NotFound("Child chunk not found.") - # The role of the current user in the ta table must be admin or owner - if not current_user.is_editor: + # 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: raise Forbidden() try: DatasetService.check_dataset_permission(dataset, current_user) @@ -624,8 +624,8 @@ class ChildChunkUpdateApi(Resource): ).first() if not child_chunk: raise NotFound("Child chunk not found.") - # The role of the current user in the ta table must be admin or owner - if not current_user.is_editor: + # 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: raise Forbidden() try: DatasetService.check_dataset_permission(dataset, current_user) diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index 1869cf67c2..749099053d 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -10,7 +10,7 @@ from controllers.service_api.app.error import NotChatAppError from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.app.entities.app_invoke_entities import InvokeFrom from fields.conversation_fields import message_file_fields -from fields.message_fields import feedback_fields, retriever_resource_fields +from fields.message_fields import agent_thought_fields, feedback_fields, retriever_resource_fields from fields.raws import FilesContainedField from libs.helper import TimestampField, uuid_value from models.model import App, AppMode, EndUser @@ -19,20 +19,6 @@ from services.message_service import MessageService class MessageListApi(Resource): - agent_thought_fields = { - "id": fields.String, - "chain_id": fields.String, - "message_id": fields.String, - "position": fields.Integer, - "thought": fields.String, - "tool": fields.String, - "tool_labels": fields.Raw, - "tool_input": fields.String, - "created_at": TimestampField, - "observation": fields.String, - "message_files": fields.List(fields.Nested(message_file_fields)), - } - message_fields = { "id": fields.String, "conversation_id": fields.String, diff --git a/api/core/model_runtime/model_providers/__base/ai_model.py b/api/core/model_runtime/model_providers/__base/ai_model.py index cdd1bba6be..bd05590018 100644 --- a/api/core/model_runtime/model_providers/__base/ai_model.py +++ b/api/core/model_runtime/model_providers/__base/ai_model.py @@ -80,7 +80,7 @@ class AIModel(BaseModel): ) ) elif isinstance(invoke_error, InvokeError): - return invoke_error(description=f"[{self.provider_name}] {invoke_error.description}, {str(error)}") + return InvokeError(description=f"[{self.provider_name}] {invoke_error.description}, {str(error)}") else: return error diff --git a/api/core/workflow/nodes/answer/base_stream_processor.py b/api/core/workflow/nodes/answer/base_stream_processor.py index 4759356ae1..f33ba6cd5d 100644 --- a/api/core/workflow/nodes/answer/base_stream_processor.py +++ b/api/core/workflow/nodes/answer/base_stream_processor.py @@ -57,11 +57,19 @@ class StreamProcessor(ABC): # The branch_identify parameter is added to ensure that # only nodes in the correct logical branch are included. + reachable_node_ids.append(edge.target_node_id) ids = self._fetch_node_ids_in_reachable_branch(edge.target_node_id, run_result.edge_source_handle) reachable_node_ids.extend(ids) else: + # if the condition edge in parallel, and the target node is not in parallel, we should not remove it + # Issues: #13626 + if ( + finished_node_id in self.graph.node_parallel_mapping + and edge.target_node_id not in self.graph.parallel_mapping + ): + continue unreachable_first_node_ids.append(edge.target_node_id) - + unreachable_first_node_ids = list(set(unreachable_first_node_ids) - set(reachable_node_ids)) for node_id in unreachable_first_node_ids: self._remove_node_ids_in_unreachable_branch(node_id, reachable_node_ids) diff --git a/api/extensions/storage/opendal_storage.py b/api/extensions/storage/opendal_storage.py index 98f646f4f9..ee8cfa9179 100644 --- a/api/extensions/storage/opendal_storage.py +++ b/api/extensions/storage/opendal_storage.py @@ -71,15 +71,8 @@ class OpenDALStorage(BaseStorage): logger.debug(f"file {filename} downloaded to {target_filepath}") def exists(self, filename: str) -> bool: - # FIXME this is a workaround for opendal python-binding do not have a exists method and no better - # error handler here when opendal python-binding has a exists method, we should use it - # more https://github.com/apache/opendal/blob/main/bindings/python/src/operator.rs - try: - res: bool = self.op.stat(path=filename).mode.is_file() - logger.debug(f"file {filename} checked") - return res - except Exception: - return False + res: bool = self.op.exists(path=filename) + return res def delete(self, filename: str): if self.exists(filename): diff --git a/api/models/__init__.py b/api/models/__init__.py index 1237c55a30..2066481a61 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -5,7 +5,6 @@ from .account import ( InvitationCode, Tenant, TenantAccountJoin, - TenantAccountJoinRole, TenantAccountRole, TenantStatus, ) @@ -156,7 +155,6 @@ __all__ = [ "TagBinding", "Tenant", "TenantAccountJoin", - "TenantAccountJoinRole", "TenantAccountRole", "TenantDefaultModel", "TenantPreferredModelProvider", diff --git a/api/models/account.py b/api/models/account.py index bac1ec1c2e..a0b8957fe1 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -220,13 +220,6 @@ class Tenant(db.Model): # type: ignore[name-defined] self.custom_config = json.dumps(value) -class TenantAccountJoinRole(enum.Enum): - OWNER = "owner" - ADMIN = "admin" - NORMAL = "normal" - DATASET_OPERATOR = "dataset_operator" - - class TenantAccountJoin(db.Model): # type: ignore[name-defined] __tablename__ = "tenant_account_joins" __table_args__ = ( diff --git a/api/poetry.lock b/api/poetry.lock index 79d49404fd..1a6397603f 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -9922,4 +9922,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "b22f8a8e798de0ca992b6fce0296081e90732dd30a00dbc3b3db24158dc857e9" +content-hash = "13cbc3af42af0b3013c4a63024c372886ed88080579b1e2e5083e3dd191d080d" diff --git a/api/pyproject.toml b/api/pyproject.toml index 419d406c53..c872a27cec 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -105,7 +105,7 @@ bce-python-sdk = "~0.9.23" cos-python-sdk-v5 = "1.9.30" esdk-obs-python = "3.24.6.1" google-cloud-storage = "2.16.0" -opendal = "~0.45.12" +opendal = "~0.45.16" oss2 = "2.18.5" supabase = "~2.8.1" tos = "~2.7.1" diff --git a/api/services/account_service.py b/api/services/account_service.py index 2923750298..42d1fba97f 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -28,7 +28,6 @@ from models.account import ( AccountStatus, Tenant, TenantAccountJoin, - TenantAccountJoinRole, TenantAccountRole, TenantStatus, ) @@ -625,8 +624,8 @@ class TenantService: @staticmethod def create_tenant_member(tenant: Tenant, account: Account, role: str = "normal") -> TenantAccountJoin: """Create tenant member""" - if role == TenantAccountJoinRole.OWNER.value: - if TenantService.has_roles(tenant, [TenantAccountJoinRole.OWNER]): + if role == TenantAccountRole.OWNER.value: + if TenantService.has_roles(tenant, [TenantAccountRole.OWNER]): logging.error(f"Tenant {tenant.id} has already an owner.") raise Exception("Tenant already has an owner.") @@ -734,10 +733,10 @@ class TenantService: return updated_accounts @staticmethod - def has_roles(tenant: Tenant, roles: list[TenantAccountJoinRole]) -> bool: + def has_roles(tenant: Tenant, roles: list[TenantAccountRole]) -> bool: """Check if user has any of the given roles for a tenant""" - if not all(isinstance(role, TenantAccountJoinRole) for role in roles): - raise ValueError("all roles must be TenantAccountJoinRole") + if not all(isinstance(role, TenantAccountRole) for role in roles): + raise ValueError("all roles must be TenantAccountRole") return ( db.session.query(TenantAccountJoin) @@ -749,7 +748,7 @@ class TenantService: ) @staticmethod - def get_user_role(account: Account, tenant: Tenant) -> Optional[TenantAccountJoinRole]: + def get_user_role(account: Account, tenant: Tenant) -> Optional[TenantAccountRole]: """Get the role of the current account for a given tenant""" join = ( db.session.query(TenantAccountJoin) diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py index 7637b31454..e012fd4296 100644 --- a/api/services/workspace_service.py +++ b/api/services/workspace_service.py @@ -2,7 +2,7 @@ from flask_login import current_user # type: ignore from configs import dify_config from extensions.ext_database import db -from models.account import Tenant, TenantAccountJoin, TenantAccountJoinRole +from models.account import Tenant, TenantAccountJoin, TenantAccountRole from services.account_service import TenantService from services.feature_service import FeatureService @@ -18,7 +18,6 @@ class WorkspaceService: "plan": tenant.plan, "status": tenant.status, "created_at": tenant.created_at, - "in_trail": True, "trial_end_reason": None, "role": "normal", } @@ -34,9 +33,7 @@ class WorkspaceService: can_replace_logo = FeatureService.get_features(tenant_info["id"]).can_replace_logo - if can_replace_logo and TenantService.has_roles( - tenant, [TenantAccountJoinRole.OWNER, TenantAccountJoinRole.ADMIN] - ): + if can_replace_logo and TenantService.has_roles(tenant, [TenantAccountRole.OWNER, TenantAccountRole.ADMIN]): base_url = dify_config.FILES_URL replace_webapp_logo = ( f"{base_url}/files/workspaces/{tenant.id}/webapp-logo" diff --git a/docker/.env.example b/docker/.env.example index 29073fa1b0..a3788ecada 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -723,6 +723,9 @@ SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128 # Maximum loop count in the workflow LOOP_NODE_MAX_COUNT=100 +# The maximum number of tools that can be used in the agent. +MAX_TOOLS_NUM=10 + # ------------------------------ # Environment Variables for web Service # ------------------------------ diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 59544600d0..2879f2194f 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -68,6 +68,7 @@ services: INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-} PM2_INSTANCES: ${PM2_INSTANCES:-2} LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} + MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} # The postgres database. db: diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 6c95ddd1f2..1d7f0ac3d8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -311,6 +311,7 @@ x-shared-env: &shared-api-worker-env SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} + MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} PGUSER: ${PGUSER:-${DB_USERNAME}} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}} @@ -486,6 +487,7 @@ services: INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-} PM2_INSTANCES: ${PM2_INSTANCES:-2} LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} + MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} # The postgres database. db: diff --git a/web/.env.example b/web/.env.example index 6738bfce07..fa1477ba79 100644 --- a/web/.env.example +++ b/web/.env.example @@ -40,3 +40,6 @@ NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000 # Maximum loop count in the workflow NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=100 + +# Maximum number of tools in the agent/workflow +NEXT_PUBLIC_MAX_TOOLS_NUM=10 \ No newline at end of file diff --git a/web/app/components/app/configuration/config/agent/agent-setting/index.tsx b/web/app/components/app/configuration/config/agent/agent-setting/index.tsx index 9fae3417b9..50903b0b6e 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/index.tsx @@ -1,8 +1,9 @@ 'use client' import type { FC } from 'react' -import React, { useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react' +import { useClickAway } from 'ahooks' import ItemPanel from './item-panel' import Button from '@/app/components/base/button' import { CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication' @@ -31,6 +32,18 @@ const AgentSetting: FC = ({ }) => { const { t } = useTranslation() const [tempPayload, setTempPayload] = useState(payload) + const ref = useRef(null) + const [mounted, setMounted] = useState(false) + + useClickAway(() => { + if (mounted) + onCancel() + }, ref) + + useEffect(() => { + setMounted(true) + }, []) + const handleSave = () => { onSave(tempPayload) } @@ -42,6 +55,7 @@ const AgentSetting: FC = ({ }} >
diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx index 7331dda54b..6388edf83d 100644 --- a/web/app/components/develop/template/template_chat.en.mdx +++ b/web/app/components/develop/template/template_chat.en.mdx @@ -641,7 +641,7 @@ Chat applications support session persistence, allowing previous chat history to "tool_input": "{\"dalle2\": {\"prompt\": \"cat\"}}", "created_at": 1705988186, "observation": "image has been created and sent to user already, you should tell user to check it now.", - "message_files": [ + "files": [ "976990d2-5294-47e6-8f14-7356ba9d2d76" ] }, @@ -655,7 +655,7 @@ Chat applications support session persistence, allowing previous chat history to "tool_input": "", "created_at": 1705988199, "observation": "", - "message_files": [] + "files": [] } ] } diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index 78f21476ab..0199951c5b 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -641,7 +641,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tool_input": "{\"dalle2\": {\"prompt\": \"cat\"}}", "created_at": 1705988186, "observation": "画像はすでに作成され、ユーザーに送信されました。今すぐユーザーに確認するように伝えてください。", - "message_files": [ + "files": [ "976990d2-5294-47e6-8f14-7356ba9d2d76" ] }, @@ -655,7 +655,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tool_input": "", "created_at": 1705988199, "observation": "", - "message_files": [] + "files": [] } ] } diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index e4a426462e..c3fcb849e1 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -657,7 +657,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tool_input": "{\"dalle2\": {\"prompt\": \"cat\"}}", "created_at": 1705988186, "observation": "image has been created and sent to user already, you should tell user to check it now.", - "message_files": [ + "files": [ "976990d2-5294-47e6-8f14-7356ba9d2d76" ] }, @@ -671,7 +671,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tool_input": "", "created_at": 1705988199, "observation": "", - "message_files": [] + "files": [] } ] } diff --git a/web/app/components/header/github-star/index.tsx b/web/app/components/header/github-star/index.tsx index 328fa8ec7c..b087b9e41a 100644 --- a/web/app/components/header/github-star/index.tsx +++ b/web/app/components/header/github-star/index.tsx @@ -17,7 +17,7 @@ const GithubStar: FC<{ className: string }> = (props) => { queryKey: ['github-star'], queryFn: getStar, enabled: process.env.NODE_ENV !== 'development', - initialData: { stargazers_count: 6000 }, + initialData: { stargazers_count: 81204 }, }) if (isFetching) return null diff --git a/web/app/components/workflow/nodes/loop/default.ts b/web/app/components/workflow/nodes/loop/default.ts index ded8afc517..67b1f6edde 100644 --- a/web/app/components/workflow/nodes/loop/default.ts +++ b/web/app/components/workflow/nodes/loop/default.ts @@ -28,9 +28,6 @@ const nodeDefault: NodeDefault = { checkValid(payload: LoopNodeType, t: any) { let errorMessages = '' - if (!errorMessages && (!payload.break_conditions || payload.break_conditions.length === 0)) - errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.loop.breakCondition') }) - payload.break_conditions!.forEach((condition) => { if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0)) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) }) diff --git a/web/app/components/workflow/nodes/loop/panel.tsx b/web/app/components/workflow/nodes/loop/panel.tsx index e416e8d77e..e361749f6d 100644 --- a/web/app/components/workflow/nodes/loop/panel.tsx +++ b/web/app/components/workflow/nodes/loop/panel.tsx @@ -55,6 +55,7 @@ const Panel: FC> = ({
{t(`${i18nPrefix}.breakCondition`)}
} + tooltip={t(`${i18nPrefix}.breakConditionTip`)} > { const { nodesReadOnly: readOnly } = useNodesReadOnly() const { isNodeInLoop } = useIsNodeInLoop(id) const isChatMode = useIsChatMode() + const conversationVariables = useStore(s => s.conversationVariables) const { inputs, setInputs } = useNodeCrud(id, payload) @@ -35,7 +37,7 @@ const useConfig = (id: string, payload: LoopNodeType) => { const beforeNodes = getBeforeNodesInSameBranch(id) const loopChildrenNodes = getLoopNodeChildren(id) const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes] - const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode) + const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables) // single run const loopInputKey = `${id}.input_selector` diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index abf377e389..df7aa0cbf4 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -1,6 +1,7 @@ 'use client' -import React, { useEffect } from 'react' +import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { useDebounceFn } from 'ahooks' import Link from 'next/link' import { useRouter } from 'next/navigation' @@ -39,7 +40,7 @@ const InstallForm = () => { const { register, handleSubmit, - formState: { errors }, + formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(accountFormSchema), defaultValues: { @@ -59,9 +60,22 @@ const InstallForm = () => { } const handleSetting = async () => { + if (isSubmitting) return handleSubmit(onSubmit)() } + const { run: debouncedHandleKeyDown } = useDebounceFn( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleSetting() + } + }, + { wait: 200 }, + ) + + const handleKeyDown = useCallback(debouncedHandleKeyDown, [debouncedHandleKeyDown]) + useEffect(() => { fetchSetupStatus().then((res: SetupStatusResponse) => { if (res.step === 'finished') { @@ -90,7 +104,7 @@ const InstallForm = () => {
-
+
@@ -114,7 +128,7 @@ const InstallForm = () => {
{errors.name && {t(`${errors.name.message}`)}} @@ -129,7 +143,7 @@ const InstallForm = () => { {...register('password')} type={showPassword ? 'text' : 'password'} placeholder={t('login.passwordPlaceholder') || ''} - className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'} + className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder:text-gray-400 caret-primary-600 sm:text-sm pr-10'} />
diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 72077567b3..8a5af7acfc 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -47,6 +47,7 @@ const LocaleLayout = ({ data-public-maintenance-notice={process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE} data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT} data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS} + data-public-max-tools-num={process.env.NEXT_PUBLIC_MAX_TOOLS_NUM} data-public-top-k-max-value={process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE} data-public-indexing-max-segmentation-tokens-length={process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH} data-public-loop-node-max-count={process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT} diff --git a/web/config/index.ts b/web/config/index.ts index 94eb0db948..3426222796 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -162,7 +162,14 @@ export const ANNOTATION_DEFAULT = { score_threshold: 0.9, } -export const MAX_TOOLS_NUM = 10 +export let maxToolsNum = 10 + +if (process.env.NEXT_PUBLIC_MAX_TOOLS_NUM && process.env.NEXT_PUBLIC_MAX_TOOLS_NUM !== '') + maxToolsNum = Number.parseInt(process.env.NEXT_PUBLIC_MAX_TOOLS_NUM) +else if (globalThis.document?.body?.getAttribute('data-public-max-tools-num') && globalThis.document.body.getAttribute('data-public-max-tools-num') !== '') + maxToolsNum = Number.parseInt(globalThis.document.body.getAttribute('data-public-max-tools-num') as string) + +export const MAX_TOOLS_NUM = maxToolsNum export const DEFAULT_AGENT_SETTING = { enabled: false, diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index a6a9afaec9..da246d5ca4 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -49,7 +49,6 @@ const initialWorkspaceInfo: ICurrentWorkspace = { created_at: 0, role: 'normal', providers: [], - in_trail: true, } const AppContext = createContext({ diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 8aa4273158..c81d9f8e94 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -703,6 +703,7 @@ const translation = { loop_other: '{{count}} Loops', currentLoop: 'Current Loop', breakCondition: 'Loop Termination Condition', + breakConditionTip: 'Only variables within loops with termination conditions and conversation variables can be referenced.', loopMaxCount: 'Maximum Loop Count', loopMaxCountError: 'Please enter a valid maximum loop count, ranging from 1 to {{maxCount}}', errorResponseMethod: 'Error Response Method', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index dc5196908e..c9bccebcea 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -704,6 +704,7 @@ const translation = { loop_other: '{{count}} 个循环', currentLoop: '当前循环', breakCondition: '循环终止条件', + breakConditionTip: '支持引用终止条件循环内的变量和会话变量。', loopMaxCount: '最大循环次数', loopMaxCountError: '请输入正确的 最大循环次数,范围为 1 到 {{maxCount}}', errorResponseMethod: '错误响应方法', diff --git a/web/models/common.ts b/web/models/common.ts index 4c25e92a85..4086220e2e 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -1,23 +1,23 @@ import type { I18nText } from '@/i18n/language' -export interface CommonResponse { +export type CommonResponse = { result: 'success' | 'fail' } -export interface OauthResponse { +export type OauthResponse = { redirect_url: string } -export interface SetupStatusResponse { +export type SetupStatusResponse = { step: 'finished' | 'not_started' setup_at?: Date } -export interface InitValidateStatusResponse { +export type InitValidateStatusResponse = { status: 'finished' | 'not_started' } -export interface UserProfileResponse { +export type UserProfileResponse = { id: string name: string email: string @@ -33,13 +33,13 @@ export interface UserProfileResponse { created_at?: string } -export interface UserProfileOriginResponse { +export type UserProfileOriginResponse = { json: () => Promise bodyUsed: boolean headers: any } -export interface LangGeniusVersionResponse { +export type LangGeniusVersionResponse = { current_version: string latest_version: string version: string @@ -49,7 +49,7 @@ export interface LangGeniusVersionResponse { current_env: string } -export interface TenantInfoResponse { +export type TenantInfoResponse = { name: string created_at: string providers: Array<{ @@ -80,14 +80,14 @@ export enum ProviderName { Tongyi = 'tongyi', ChatGLM = 'chatglm', } -export interface ProviderAzureToken { +export type ProviderAzureToken = { openai_api_base?: string openai_api_key?: string } -export interface ProviderAnthropicToken { +export type ProviderAnthropicToken = { anthropic_api_key?: string } -export interface ProviderTokenType { +export type ProviderTokenType = { [ProviderName.OPENAI]: string [ProviderName.AZURE_OPENAI]: ProviderAzureToken [ProviderName.ANTHROPIC]: ProviderAnthropicToken @@ -110,14 +110,14 @@ export type ProviderHosted = Provider & { quota_used: number } -export interface AccountIntegrate { +export type AccountIntegrate = { provider: 'google' | 'github' created_at: number is_bound: boolean link: string } -export interface IWorkspace { +export type IWorkspace = { id: string name: string plan: string @@ -129,7 +129,6 @@ export interface IWorkspace { export type ICurrentWorkspace = Omit & { role: 'owner' | 'admin' | 'editor' | 'dataset_operator' | 'normal' providers: Provider[] - in_trail: boolean trial_end_reason?: string custom_config?: { remove_webapp_brand?: boolean @@ -137,7 +136,7 @@ export type ICurrentWorkspace = Omit & { } } -export interface DataSourceNotionPage { +export type DataSourceNotionPage = { page_icon: null | { type: string | null url: string | null @@ -156,7 +155,7 @@ export type NotionPage = DataSourceNotionPage & { export type DataSourceNotionPageMap = Record -export interface DataSourceNotionWorkspace { +export type DataSourceNotionWorkspace = { workspace_name: string workspace_id: string workspace_icon: string | null @@ -166,7 +165,7 @@ export interface DataSourceNotionWorkspace { export type DataSourceNotionWorkspaceMap = Record -export interface DataSourceNotion { +export type DataSourceNotion = { id: string provider: string is_bound: boolean @@ -181,12 +180,12 @@ export enum DataSourceProvider { jinaReader = 'jinareader', } -export interface FirecrawlConfig { +export type FirecrawlConfig = { api_key: string base_url: string } -export interface DataSourceItem { +export type DataSourceItem = { id: string category: DataSourceCategory provider: DataSourceProvider @@ -195,15 +194,15 @@ export interface DataSourceItem { updated_at: number } -export interface DataSources { +export type DataSources = { sources: DataSourceItem[] } -export interface GithubRepo { +export type GithubRepo = { stargazers_count: number } -export interface PluginProvider { +export type PluginProvider = { tool_name: string is_enabled: boolean credentials: { @@ -211,7 +210,7 @@ export interface PluginProvider { } | null } -export interface FileUploadConfigResponse { +export type FileUploadConfigResponse = { batch_count_limit: number image_file_size_limit?: number | string // default is 10MB file_size_limit: number // default is 15MB @@ -234,14 +233,14 @@ export type InvitationResponse = CommonResponse & { invitation_results: InvitationResult[] } -export interface ApiBasedExtension { +export type ApiBasedExtension = { id?: string name?: string api_endpoint?: string api_key?: string } -export interface CodeBasedExtensionForm { +export type CodeBasedExtensionForm = { type: string label: I18nText variable: string @@ -252,17 +251,17 @@ export interface CodeBasedExtensionForm { max_length?: number } -export interface CodeBasedExtensionItem { +export type CodeBasedExtensionItem = { name: string label: any form_schema: CodeBasedExtensionForm[] } -export interface CodeBasedExtension { +export type CodeBasedExtension = { module: string data: CodeBasedExtensionItem[] } -export interface ExternalDataTool { +export type ExternalDataTool = { type?: string label?: string icon?: string @@ -274,7 +273,7 @@ export interface ExternalDataTool { } & Partial> } -export interface ModerateResponse { +export type ModerateResponse = { flagged: boolean text: string }