diff --git a/api/commands.py b/api/commands.py index 604fdd9c40..10e08f9d37 100644 --- a/api/commands.py +++ b/api/commands.py @@ -1650,7 +1650,7 @@ def transform_datasource_credentials(): "integration_secret": api_key, } datasource_provider = DatasourceProvider( - provider="jina", + provider="jinareader", tenant_id=tenant_id, plugin_id=jina_plugin_id, auth_type=api_key_credential_type.value, diff --git a/api/configs/middleware/vdb/weaviate_config.py b/api/configs/middleware/vdb/weaviate_config.py index 6a79412ab8..aa81c870f6 100644 --- a/api/configs/middleware/vdb/weaviate_config.py +++ b/api/configs/middleware/vdb/weaviate_config.py @@ -22,6 +22,11 @@ class WeaviateConfig(BaseSettings): default=True, ) + WEAVIATE_GRPC_ENDPOINT: str | None = Field( + description="URL of the Weaviate gRPC server (e.g., 'grpc://localhost:50051' or 'grpcs://weaviate.example.com:443')", + default=None, + ) + WEAVIATE_BATCH_SIZE: PositiveInt = Field( description="Number of objects to be processed in a single batch operation (default is 100)", default=100, diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 7481b0071c..02167245be 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -112,7 +112,18 @@ class DraftWorkflowApi(Resource): }, ) ) - @api.response(200, "Draft workflow synced successfully", workflow_fields) + @api.response( + 200, + "Draft workflow synced successfully", + api.model( + "SyncDraftWorkflowResponse", + { + "result": fields.String, + "hash": fields.String, + "updated_at": fields.String, + }, + ), + ) @api.response(400, "Invalid workflow configuration") @api.response(403, "Permission denied") @edit_permission_required diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index 6250184986..c07e18c686 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -68,6 +68,7 @@ def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: Fe kwargs["app_model"] = app_model + # If caller needs end-user context, attach EndUser to current_user if fetch_user_arg: if fetch_user_arg.fetch_from == WhereisUserArg.QUERY: user_id = request.args.get("user") @@ -76,7 +77,6 @@ def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: Fe elif fetch_user_arg.fetch_from == WhereisUserArg.FORM: user_id = request.form.get("user") else: - # use default-user user_id = None if not user_id and fetch_user_arg.required: @@ -91,6 +91,28 @@ def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: Fe # Set EndUser as current logged-in user for flask_login.current_user current_app.login_manager._update_request_context_with_user(end_user) # type: ignore user_logged_in.send(current_app._get_current_object(), user=end_user) # type: ignore + else: + # For service API without end-user context, ensure an Account is logged in + # so services relying on current_account_with_tenant() work correctly. + tenant_owner_info = ( + db.session.query(Tenant, Account) + .join(TenantAccountJoin, Tenant.id == TenantAccountJoin.tenant_id) + .join(Account, TenantAccountJoin.account_id == Account.id) + .where( + Tenant.id == app_model.tenant_id, + TenantAccountJoin.role == "owner", + Tenant.status == TenantStatus.NORMAL, + ) + .one_or_none() + ) + + if tenant_owner_info: + tenant_model, account = tenant_owner_info + account.current_tenant = tenant_model + current_app.login_manager._update_request_context_with_user(account) # type: ignore + user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore + else: + raise Unauthorized("Tenant owner account not found or tenant is not active.") return view_func(*args, **kwargs) diff --git a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py index dceade0af9..39eb91e50e 100644 --- a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py +++ b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py @@ -39,11 +39,13 @@ class WeaviateConfig(BaseModel): Attributes: endpoint: Weaviate server endpoint URL + grpc_endpoint: Optional Weaviate gRPC server endpoint URL api_key: Optional API key for authentication batch_size: Number of objects to batch per insert operation """ endpoint: str + grpc_endpoint: str | None = None api_key: str | None = None batch_size: int = 100 @@ -88,9 +90,22 @@ class WeaviateVector(BaseVector): http_secure = p.scheme == "https" http_port = p.port or (443 if http_secure else 80) - grpc_host = host - grpc_secure = http_secure - grpc_port = 443 if grpc_secure else 50051 + # Parse gRPC configuration + if config.grpc_endpoint: + # Urls without scheme won't be parsed correctly in some python verions, + # see https://bugs.python.org/issue27657 + grpc_endpoint_with_scheme = ( + config.grpc_endpoint if "://" in config.grpc_endpoint else f"grpc://{config.grpc_endpoint}" + ) + grpc_p = urlparse(grpc_endpoint_with_scheme) + grpc_host = grpc_p.hostname or "localhost" + grpc_port = grpc_p.port or (443 if grpc_p.scheme == "grpcs" else 50051) + grpc_secure = grpc_p.scheme == "grpcs" + else: + # Infer from HTTP endpoint as fallback + grpc_host = host + grpc_secure = http_secure + grpc_port = 443 if grpc_secure else 50051 client = weaviate.connect_to_custom( http_host=host, @@ -432,6 +447,7 @@ class WeaviateVectorFactory(AbstractVectorFactory): collection_name=collection_name, config=WeaviateConfig( endpoint=dify_config.WEAVIATE_ENDPOINT or "", + grpc_endpoint=dify_config.WEAVIATE_GRPC_ENDPOINT or "", api_key=dify_config.WEAVIATE_API_KEY, batch_size=dify_config.WEAVIATE_BATCH_SIZE, ), diff --git a/api/core/tools/__base/tool.py b/api/core/tools/__base/tool.py index 82616596f8..8ca4eabb7a 100644 --- a/api/core/tools/__base/tool.py +++ b/api/core/tools/__base/tool.py @@ -210,12 +210,13 @@ class Tool(ABC): meta=meta, ) - def create_json_message(self, object: dict) -> ToolInvokeMessage: + def create_json_message(self, object: dict, suppress_output: bool = False) -> ToolInvokeMessage: """ create a json message """ return ToolInvokeMessage( - type=ToolInvokeMessage.MessageType.JSON, message=ToolInvokeMessage.JsonMessage(json_object=object) + type=ToolInvokeMessage.MessageType.JSON, + message=ToolInvokeMessage.JsonMessage(json_object=object, suppress_output=suppress_output), ) def create_variable_message( diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 72d319ef4d..353f3a646a 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -129,6 +129,7 @@ class ToolInvokeMessage(BaseModel): class JsonMessage(BaseModel): json_object: dict + suppress_output: bool = Field(default=False, description="Whether to suppress JSON output in result string") class BlobMessage(BaseModel): blob: bytes diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 92d441b5ac..13fd579e20 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -245,6 +245,9 @@ class ToolEngine: + "you do not need to create it, just tell the user to check it now." ) elif response.type == ToolInvokeMessage.MessageType.JSON: + json_message = cast(ToolInvokeMessage.JsonMessage, response.message) + if json_message.suppress_output: + continue json_parts.append( json.dumps( safe_json_value(cast(ToolInvokeMessage.JsonMessage, response.message).json_object), diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 2cd46647a0..5703c19c88 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -117,7 +117,7 @@ class WorkflowTool(Tool): self._latest_usage = self._derive_usage_from_result(data) yield self.create_text_message(json.dumps(outputs, ensure_ascii=False)) - yield self.create_json_message(outputs) + yield self.create_json_message(outputs, suppress_output=True) @property def latest_usage(self) -> LLMUsage: diff --git a/docker/.env.example b/docker/.env.example index f6f5a7e2b4..c979110b40 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -497,6 +497,7 @@ VECTOR_INDEX_NAME_PREFIX=Vector_index # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. WEAVIATE_ENDPOINT=http://weaviate:8080 WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih +WEAVIATE_GRPC_ENDPOINT=grpc://weaviate:50051 # The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`. QDRANT_URL=http://qdrant:6333 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e6df429ace..c2ffc6c374 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -158,6 +158,7 @@ x-shared-env: &shared-api-worker-env VECTOR_INDEX_NAME_PREFIX: ${VECTOR_INDEX_NAME_PREFIX:-Vector_index} WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} + WEAVIATE_GRPC_ENDPOINT: ${WEAVIATE_GRPC_ENDPOINT:-grpc://weaviate:50051} QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333} QDRANT_API_KEY: ${QDRANT_API_KEY:-difyai123456} QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20}