This commit is contained in:
chariri 2026-06-25 18:39:28 +00:00 committed by GitHub
commit 75915b4469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 951 additions and 383 deletions

View File

@ -167,12 +167,16 @@ register_schema_models(
ChatMessagesQuery,
MessageFeedbackPayload,
FeedbackExportQuery,
)
register_response_schema_models(
console_ns,
AnnotationCountResponse,
SuggestedQuestionsResponse,
MessageDetailResponse,
MessageInfiniteScrollPaginationResponse,
SimpleResultResponse,
TextFileResponse,
)
register_response_schema_models(console_ns, SimpleResultResponse, TextFileResponse)
@console_ns.route("/apps/<uuid:app_id>/chat-messages")

View File

@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.common.fields import RedirectResponse, SimpleResultResponse
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
@ -19,11 +19,13 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from core.entities.provider_entities import ProviderConfig
from core.plugin.entities.plugin_daemon import PluginOAuthAuthorizationUrlResponse
from core.plugin.impl.oauth import OAuthHandler
from core.tools.entities.common_entities import I18nObject
from fields.base import ResponseModel
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import dump_response
from libs.login import login_required
from models import Account
from models.provider_ids import DatasourceProviderID
@ -33,7 +35,9 @@ from services.plugin.oauth_service import OAuthProxyService
class DatasourceCredentialPayload(BaseModel):
name: str | None = Field(default=None, max_length=100)
credentials: dict[str, Any]
credentials: dict[str, Any] = Field(
description="Plugin-defined credential parameters. The schema is declared by the datasource provider."
)
class DatasourceCredentialDeletePayload(BaseModel):
@ -43,11 +47,17 @@ class DatasourceCredentialDeletePayload(BaseModel):
class DatasourceCredentialUpdatePayload(BaseModel):
credential_id: str
name: str | None = Field(default=None, max_length=100)
credentials: dict[str, Any] | None = Field(default=None)
credentials: dict[str, Any] | None = Field(
default=None,
description="Plugin-defined credential parameters. The schema is declared by the datasource provider.",
)
class DatasourceCustomClientPayload(BaseModel):
client_params: dict[str, Any] | None = Field(default=None)
client_params: dict[str, Any] | None = Field(
default=None,
description="Plugin-defined OAuth client parameters. The schema is declared by the datasource provider.",
)
enable_oauth_custom_client: bool | None = None
@ -71,8 +81,48 @@ class DatasourceOAuthCallbackQuery(BaseModel):
context_id: str | None = Field(default=None, description="OAuth proxy context ID")
class DatasourceCredentialsResponse(ResponseModel):
result: Any
class DatasourceCredentialResponse(ResponseModel):
credential: dict[str, Any] = Field(
description="Obfuscated plugin-defined credential parameters from the datasource provider."
)
type: str
name: str
avatar_url: str | None
id: str
is_default: bool
class DatasourceCredentialListResponse(ResponseModel):
result: list[DatasourceCredentialResponse]
class DatasourceOAuthSchemaResponse(ResponseModel):
client_schema: list[ProviderConfig]
credentials_schema: list[ProviderConfig]
oauth_custom_client_params: dict[str, Any] | None = Field(
description="Masked plugin-defined OAuth client parameters, when configured for the tenant."
)
is_oauth_custom_client_enabled: bool
is_system_oauth_params_exists: bool
redirect_uri: str
class DatasourceProviderAuthResponse(ResponseModel):
author: str
provider: str
plugin_id: str
plugin_unique_identifier: str
icon: str
name: str
label: I18nObject
description: I18nObject
credential_schema: list[ProviderConfig]
oauth_schema: DatasourceOAuthSchemaResponse | None
credentials_list: list[DatasourceCredentialResponse]
class DatasourceProviderAuthListResponse(ResponseModel):
result: list[DatasourceProviderAuthResponse]
register_schema_models(
@ -88,9 +138,9 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
DatasourceCredentialsResponse,
DatasourceCredentialListResponse,
DatasourceProviderAuthListResponse,
PluginOAuthAuthorizationUrlResponse,
RedirectResponse,
SimpleResultResponse,
)
@ -100,7 +150,7 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
@console_ns.doc(params=query_params_from_model(DatasourceOAuthAuthorizationQuery))
@console_ns.response(
200,
"Authorization URL retrieved successfully",
"Datasource OAuth authorization URL generated successfully",
console_ns.models[PluginOAuthAuthorizationUrlResponse.__name__],
)
@setup_required
@ -140,7 +190,8 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
redirect_uri=redirect_uri,
system_credentials=oauth_config,
)
response = make_response(jsonable_encoder(authorization_url_response))
# response-contract:ignore cookie-bearing Flask response
response = make_response(dump_response(PluginOAuthAuthorizationUrlResponse, authorization_url_response))
response.set_cookie(
"context_id",
context_id,
@ -154,11 +205,8 @@ class DatasourcePluginOAuthAuthorizationUrl(Resource):
@console_ns.route("/oauth/plugin/<path:provider_id>/datasource/callback")
class DatasourceOAuthCallback(Resource):
@console_ns.doc(params=query_params_from_model(DatasourceOAuthCallbackQuery))
@console_ns.response(
302,
"Redirect to console OAuth callback page",
console_ns.models[RedirectResponse.__name__],
)
# response-contract:ignore redirect response
@console_ns.response(302, "Redirect to OAuth callback page")
@setup_required
def get(self, provider_id: str):
context_id = request.cookies.get("context_id") or request.args.get("context_id")
@ -217,7 +265,9 @@ class DatasourceOAuthCallback(Resource):
@console_ns.route("/auth/plugin/datasource/<path:provider_id>")
class DatasourceAuth(Resource):
@console_ns.expect(console_ns.models[DatasourceCredentialPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(
200, "Datasource credential created successfully", console_ns.models[SimpleResultResponse.__name__]
)
@setup_required
@login_required
@account_initialization_required
@ -238,12 +288,16 @@ class DatasourceAuth(Resource):
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.response(
200,
"Datasource credentials retrieved successfully",
console_ns.models[DatasourceCredentialListResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, user: Account, provider_id: str):
@ -256,7 +310,7 @@ class DatasourceAuth(Resource):
plugin_id=datasource_provider_id.plugin_id,
user=user,
)
return {"result": datasources}, 200
return dump_response(DatasourceCredentialListResponse, {"result": datasources}), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/delete")
@ -282,13 +336,15 @@ class DatasourceAuthDeleteApi(Resource):
provider=provider_name,
plugin_id=plugin_id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/update")
class DatasourceAuthUpdateApi(Resource):
@console_ns.expect(console_ns.models[DatasourceCredentialUpdatePayload.__name__])
@console_ns.response(201, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(
201, "Datasource credential updated successfully", console_ns.models[SimpleResultResponse.__name__]
)
@setup_required
@login_required
@account_initialization_required
@ -308,12 +364,16 @@ class DatasourceAuthUpdateApi(Resource):
credentials=payload.credentials or {},
name=payload.name,
)
return {"result": "success"}, 201
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
@console_ns.route("/auth/plugin/datasource/list")
class DatasourceAuthListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@console_ns.response(
200,
"Datasource credentials retrieved successfully",
console_ns.models[DatasourceProviderAuthListResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -321,12 +381,16 @@ class DatasourceAuthListApi(Resource):
def get(self, current_tenant_id: str):
datasource_provider_service = DatasourceProviderService()
datasources = datasource_provider_service.get_all_datasource_credentials(tenant_id=current_tenant_id)
return {"result": jsonable_encoder(datasources)}, 200
return dump_response(DatasourceProviderAuthListResponse, {"result": datasources}), 200
@console_ns.route("/auth/plugin/datasource/default-list")
class DatasourceHardCodeAuthListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@console_ns.response(
200,
"Default datasource credentials retrieved successfully",
console_ns.models[DatasourceProviderAuthListResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -334,13 +398,15 @@ class DatasourceHardCodeAuthListApi(Resource):
def get(self, current_tenant_id: str):
datasource_provider_service = DatasourceProviderService()
datasources = datasource_provider_service.get_hard_code_datasource_credentials(tenant_id=current_tenant_id)
return {"result": jsonable_encoder(datasources)}, 200
return dump_response(DatasourceProviderAuthListResponse, {"result": datasources}), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/custom-client")
class DatasourceAuthOauthCustomClient(Resource):
@console_ns.expect(console_ns.models[DatasourceCustomClientPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(
200, "Datasource OAuth custom client saved successfully", console_ns.models[SimpleResultResponse.__name__]
)
@setup_required
@login_required
@account_initialization_required
@ -357,7 +423,7 @@ class DatasourceAuthOauthCustomClient(Resource):
client_params=payload.client_params or {},
enabled=payload.enable_oauth_custom_client or False,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@setup_required
@login_required
@ -371,7 +437,7 @@ class DatasourceAuthOauthCustomClient(Resource):
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/default")
@ -393,7 +459,7 @@ class DatasourceAuthDefaultApi(Resource):
datasource_provider_id=datasource_provider_id,
credential_id=payload.id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@console_ns.route("/auth/plugin/datasource/<path:provider_id>/update-name")
@ -416,4 +482,4 @@ class DatasourceUpdateProviderNameApi(Resource):
name=payload.name,
credential_id=payload.credential_id,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200

View File

@ -3,9 +3,9 @@ from typing import Any
from flask_restx import ( # type: ignore
Resource, # type: ignore
)
from pydantic import BaseModel, RootModel
from pydantic import BaseModel
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import register_schema_models
from controllers.console import console_ns
from controllers.console.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
@ -21,18 +21,13 @@ class Parser(BaseModel):
credential_id: str | None = None
class DataSourceContentPreviewResponse(RootModel[Any]):
root: Any
register_schema_models(console_ns, Parser)
register_response_schema_models(console_ns, DataSourceContentPreviewResponse)
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/datasource/nodes/<string:node_id>/preview")
class DataSourceContentPreviewApi(Resource):
@console_ns.expect(console_ns.models[Parser.__name__])
@console_ns.response(200, "Success", console_ns.models[DataSourceContentPreviewResponse.__name__])
@console_ns.response(200, "Success")
@setup_required
@login_required
@account_initialization_required

View File

@ -1,34 +1,30 @@
from collections.abc import Generator
from datetime import datetime
from typing import Any
from uuid import UUID
from flask import request
from pydantic import BaseModel, Field, RootModel
from pydantic import BaseModel, Field, RootModel, field_validator
from sqlalchemy import select
from werkzeug.exceptions import Forbidden, NotFound
import services
from controllers.common.errors import FilenameNotExistsError, NoFileUploadedError, TooManyFilesError
from controllers.common.fields import GeneratedAppResponse
from controllers.common.schema import (
query_params_from_model,
query_params_from_request,
register_response_schema_models,
register_schema_model,
register_schema_models,
)
from controllers.service_api import service_api_ns
from controllers.service_api.dataset.error import PipelineRunError
from controllers.service_api.dataset.rag_pipeline.serializers import serialize_upload_file
from controllers.service_api.schema import (
event_stream_response,
json_or_event_stream_response,
multipart_file_params,
)
from controllers.service_api.schema import event_stream_response, json_or_event_stream_response, multipart_file_params
from controllers.service_api.wraps import DatasetApiResource
from core.app.apps.pipeline.pipeline_generator import PipelineGenerator
from core.app.entities.app_invoke_entities import InvokeFrom
from fields.base import ResponseModel
from libs import helper
from libs.helper import dump_response
from libs.login import current_user
from models import Account
from models.dataset import Dataset, Pipeline
@ -82,7 +78,7 @@ class DatasourcePluginResponse(ResponseModel):
datasource_type: str | None = None
title: str | None = None
user_input_variables: list[dict[str, Any]] = Field(default_factory=list)
credentials: list[DatasourceCredentialInfoResponse]
credentials: list[DatasourceCredentialInfoResponse] = Field(default_factory=list)
class DatasourcePluginListResponse(RootModel[list[DatasourcePluginResponse]]):
@ -98,14 +94,22 @@ class PipelineUploadFileResponse(ResponseModel):
created_by: str
created_at: str | None = None
@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | str | None) -> str | None:
if isinstance(value, datetime):
return value.isoformat()
return value
register_schema_model(service_api_ns, DatasourceNodeRunPayload)
register_schema_model(service_api_ns, DatasourcePluginsQuery)
register_schema_model(service_api_ns, PipelineRunApiEntity)
register_schema_models(service_api_ns, DatasourcePluginsQuery)
register_response_schema_models(
service_api_ns,
DatasourceCredentialInfoResponse,
DatasourcePluginResponse,
DatasourcePluginListResponse,
GeneratedAppResponse,
PipelineUploadFileResponse,
)
@ -117,8 +121,8 @@ class DatasourcePluginsApi(DatasetApiResource):
@service_api_ns.doc(
summary="List Datasource Plugins",
description=(
"List the datasource nodes configured in the knowledge pipeline. Each node includes the "
"plugin it uses plus the metadata needed to run it."
"List the datasource nodes configured in the knowledge pipeline. Each node includes the plugin it uses "
"plus the metadata needed to run it."
),
tags=["Knowledge Pipeline"],
responses={
@ -150,14 +154,13 @@ class DatasourcePluginsApi(DatasetApiResource):
if not dataset:
raise NotFound("Dataset not found.")
# Get query parameter to determine published or draft
is_published: bool = request.args.get("is_published", default=True, type=bool)
query = query_params_from_request(DatasourcePluginsQuery)
rag_pipeline_service: RagPipelineService = RagPipelineService()
datasource_plugins: list[dict[Any, Any]] = rag_pipeline_service.get_datasource_plugins(
tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=is_published
tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=query.is_published
)
return datasource_plugins, 200
return dump_response(DatasourcePluginListResponse, datasource_plugins), 200
@service_api_ns.route("/datasets/<uuid:dataset_id>/pipeline/datasource/nodes/<string:node_id>/run")
@ -167,8 +170,8 @@ class DatasourceNodeRunApi(DatasetApiResource):
@service_api_ns.doc(
summary="Run Datasource Node",
description=(
"Execute a single datasource node within the knowledge pipeline. Returns a streaming "
"response with the node execution results."
"Execute a single datasource node within the knowledge pipeline. Returns a streaming response with the "
"node execution results."
),
tags=["Knowledge Pipeline"],
responses={
@ -187,11 +190,6 @@ class DatasourceNodeRunApi(DatasetApiResource):
}
)
@service_api_ns.expect(service_api_ns.models[DatasourceNodeRunPayload.__name__])
@service_api_ns.response(
200,
"Datasource node run successfully",
service_api_ns.models[GeneratedAppResponse.__name__],
)
def post(self, tenant_id: str, dataset_id: UUID, node_id: str):
"""Resource for getting datasource plugins."""
dataset_id_str = str(dataset_id)
@ -208,10 +206,11 @@ class DatasourceNodeRunApi(DatasetApiResource):
datasource_node_run_api_entity = DatasourceNodeRunApiEntity.model_validate(
{
**payload.model_dump(exclude_none=True),
"pipeline_id": str(pipeline.id),
"pipeline_id": pipeline.id,
"node_id": node_id,
}
)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(
PipelineGenerator.convert_to_event_stream(
rag_pipeline_service.run_datasource_workflow_node(
@ -234,8 +233,8 @@ class PipelineRunApi(DatasetApiResource):
@service_api_ns.doc(
summary="Run Pipeline",
description=(
"Execute the full knowledge pipeline for a knowledge base. Supports both streaming and "
"blocking response modes."
"Execute the full knowledge pipeline for a knowledge base. Supports both streaming and blocking response "
"modes."
),
tags=["Knowledge Pipeline"],
responses={
@ -259,11 +258,6 @@ class PipelineRunApi(DatasetApiResource):
}
)
@service_api_ns.expect(service_api_ns.models[PipelineRunApiEntity.__name__])
@service_api_ns.response(
200,
"Pipeline run successfully",
service_api_ns.models[GeneratedAppResponse.__name__],
)
def post(self, tenant_id: str, dataset_id: UUID):
"""Resource for running a rag pipeline."""
dataset_id_str = str(dataset_id)
@ -289,6 +283,7 @@ class PipelineRunApi(DatasetApiResource):
streaming=payload.response_mode == "streaming",
)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except Exception as ex:
raise PipelineRunError(description=str(ex))
@ -364,4 +359,4 @@ class KnowledgebasePipelineFileUploadApi(DatasetApiResource):
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
return serialize_upload_file(upload_file), 201
return dump_response(PipelineUploadFileResponse, upload_file), 201

View File

@ -1,32 +0,0 @@
"""
Serialization helpers for Service API knowledge pipeline endpoints.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
from models.model import UploadFile
class UploadFileDict(TypedDict):
id: str
name: str
size: int
extension: str
mime_type: str | None
created_by: str
created_at: str | None
def serialize_upload_file(upload_file: UploadFile) -> UploadFileDict:
return {
"id": upload_file.id,
"name": upload_file.name,
"size": upload_file.size,
"extension": upload_file.extension,
"mime_type": upload_file.mime_type,
"created_by": upload_file.created_by,
"created_at": upload_file.created_at.isoformat() if upload_file.created_at else None,
}

View File

@ -4496,14 +4496,14 @@ Refresh MCP server configuration and regenerate server code
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
| 200 | Default datasource credentials retrieved successfully | **application/json**: [DatasourceProviderAuthListResponse](#datasourceproviderauthlistresponse)<br> |
### [GET] /auth/plugin/datasource/list
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
| 200 | Datasource credentials retrieved successfully | **application/json**: [DatasourceProviderAuthListResponse](#datasourceproviderauthlistresponse)<br> |
### [GET] /auth/plugin/datasource/{provider_id}
#### Parameters
@ -4516,7 +4516,7 @@ Refresh MCP server configuration and regenerate server code
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [DatasourceCredentialsResponse](#datasourcecredentialsresponse)<br> |
| 200 | Datasource credentials retrieved successfully | **application/json**: [DatasourceCredentialListResponse](#datasourcecredentiallistresponse)<br> |
### [POST] /auth/plugin/datasource/{provider_id}
#### Parameters
@ -4535,7 +4535,7 @@ Refresh MCP server configuration and regenerate server code
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 200 | Datasource credential created successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
### [DELETE] /auth/plugin/datasource/{provider_id}/custom-client
#### Parameters
@ -4567,7 +4567,7 @@ Refresh MCP server configuration and regenerate server code
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 200 | Datasource OAuth custom client saved successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
### [POST] /auth/plugin/datasource/{provider_id}/default
#### Parameters
@ -4624,7 +4624,7 @@ Refresh MCP server configuration and regenerate server code
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 201 | Datasource credential updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
### [POST] /auth/plugin/datasource/{provider_id}/update-name
#### Parameters
@ -7022,9 +7022,9 @@ Initiate OAuth login process
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 302 | Redirect to console OAuth callback page | **application/json**: [RedirectResponse](#redirectresponse)<br> |
| Code | Description |
| ---- | ----------- |
| 302 | Redirect to OAuth callback page |
### [GET] /oauth/plugin/{provider_id}/datasource/get-authorization-url
#### Parameters
@ -7038,7 +7038,7 @@ Initiate OAuth login process
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Authorization URL retrieved successfully | **application/json**: [PluginOAuthAuthorizationUrlResponse](#pluginoauthauthorizationurlresponse)<br> |
| 200 | Datasource OAuth authorization URL generated successfully | **application/json**: [PluginOAuthAuthorizationUrlResponse](#pluginoauthauthorizationurlresponse)<br> |
### [GET] /oauth/plugin/{provider}/tool/authorization-url
#### Parameters
@ -7861,9 +7861,9 @@ Initiate OAuth login process
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [DataSourceContentPreviewResponse](#datasourcecontentpreviewresponse)<br> |
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### [POST] /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/run
**Run rag pipeline datasource**
@ -13429,7 +13429,6 @@ Soft lifecycle state for Agent records.
| created_at | integer | | No |
| files | [ string ] | | Yes |
| id | string | | Yes |
| message_chain_id | string | | No |
| message_id | string | | Yes |
| observation | string | | No |
| position | integer | | Yes |
@ -13909,6 +13908,12 @@ AppMCPServer Status Enum
| use_icon_as_answer_icon | boolean | | No |
| workflow | [WorkflowPartial](#workflowpartial) | | No |
#### AppSelectorScope
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppSelectorScope | string | | |
#### AppSiteResponse
| Name | Type | Description | Required |
@ -14540,8 +14545,8 @@ Enum class for configurate method of provider model.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| annotation_create_account | [SimpleAccount](#simpleaccount) | | No |
| annotation_id | string | | Yes |
| created_at | integer | | No |
| id | string | | Yes |
#### ConversationDetail
@ -14851,12 +14856,6 @@ Model class for provider custom model configuration.
| ---- | ---- | ----------- | -------- |
| info_list | [InfoList](#infolist) | | Yes |
#### DataSourceContentPreviewResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| DataSourceContentPreviewResponse | | | |
#### DataSourceIntegrateIconResponse
| Name | Type | Description | Required |
@ -15377,32 +15376,43 @@ Model class for provider custom model configuration.
| ---- | ---- | ----------- | -------- |
| credential_id | string | | Yes |
#### DatasourceCredentialListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| result | [ [DatasourceCredentialResponse](#datasourcecredentialresponse) ] | | Yes |
#### DatasourceCredentialPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| credentials | object | | Yes |
| credentials | object | Plugin-defined credential parameters. The schema is declared by the datasource provider. | Yes |
| name | string | | No |
#### DatasourceCredentialResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatar_url | string | | Yes |
| credential | object | Obfuscated plugin-defined credential parameters from the datasource provider. | Yes |
| id | string | | Yes |
| is_default | boolean | | Yes |
| name | string | | Yes |
| type | string | | Yes |
#### DatasourceCredentialUpdatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| credential_id | string | | Yes |
| credentials | object | | No |
| credentials | object | Plugin-defined credential parameters. The schema is declared by the datasource provider. | No |
| name | string | | No |
#### DatasourceCredentialsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| result | | | Yes |
#### DatasourceCustomClientPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| client_params | object | | No |
| client_params | object | Plugin-defined OAuth client parameters. The schema is declared by the datasource provider. | No |
| enable_oauth_custom_client | boolean | | No |
#### DatasourceDefaultPayload
@ -15434,6 +15444,39 @@ Model class for provider custom model configuration.
| error | string | Error message from OAuth provider | No |
| state | string | OAuth state parameter | No |
#### DatasourceOAuthSchemaResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| client_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
| credentials_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
| is_oauth_custom_client_enabled | boolean | | Yes |
| is_system_oauth_params_exists | boolean | | Yes |
| oauth_custom_client_params | object | Masked plugin-defined OAuth client parameters, when configured for the tenant. | Yes |
| redirect_uri | string | | Yes |
#### DatasourceProviderAuthListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| result | [ [DatasourceProviderAuthResponse](#datasourceproviderauthresponse) ] | | Yes |
#### DatasourceProviderAuthResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| author | string | | Yes |
| credential_schema | [ [ProviderConfig](#providerconfig) ] | | Yes |
| credentials_list | [ [DatasourceCredentialResponse](#datasourcecredentialresponse) ] | | Yes |
| description | [I18nObject](#i18nobject) | | Yes |
| icon | string | | Yes |
| label | [I18nObject](#i18nobject) | | Yes |
| name | string | | Yes |
| oauth_schema | [DatasourceOAuthSchemaResponse](#datasourceoauthschemaresponse) | | Yes |
| plugin_id | string | | Yes |
| plugin_unique_identifier | string | | Yes |
| provider | string | | Yes |
#### DatasourceUpdateNamePayload
| Name | Type | Description | Required |
@ -17079,6 +17122,7 @@ Enum class for large language model mode.
| agent_thoughts | [ [AgentThought](#agentthought) ] | | No |
| annotation | [ConversationAnnotation](#conversationannotation) | | No |
| annotation_hit_history | [ConversationAnnotationHitHistory](#conversationannotationhithistory) | | No |
| answer | string | | Yes |
| answer_tokens | integer | | No |
| conversation_id | string | | Yes |
| created_at | integer | | No |
@ -17092,12 +17136,11 @@ Enum class for large language model mode.
| inputs | object | | Yes |
| message | [JSONValue](#jsonvalue) | | No |
| message_files | [ [MessageFile](#messagefile) ] | | No |
| message_metadata_dict | [JSONValue](#jsonvalue) | | No |
| message_tokens | integer | | No |
| metadata | [JSONValue](#jsonvalue) | | No |
| parent_message_id | string | | No |
| provider_response_latency | number | | No |
| query | string | | Yes |
| re_sign_file_url_answer | string | | Yes |
| status | string | | Yes |
| workflow_run_id | string | | No |
@ -17281,6 +17324,12 @@ Enum class for model property key.
| ---- | ---- | ----------- | -------- |
| payment_link | string | | Yes |
#### ModelSelectorScope
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| ModelSelectorScope | string | | |
#### ModelStatus
Enum class for model status.
@ -17588,6 +17637,13 @@ Coarse node-level status used by Inspector to pick a banner.
| ---- | ---- | ----------- | -------- |
| OpaqueObjectResponse | object | | |
#### Option
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| label | [I18nObject](#i18nobject) | The label of the option | Yes |
| value | string | The value of the option | Yes |
#### OutputErrorStrategy
Per-output failure handling strategy.
@ -18429,6 +18485,24 @@ Dataset Process Rule Mode
| ---- | ---- | ----------- | -------- |
| ProcessRuleMode | string | Dataset Process Rule Mode | |
#### ProviderConfig
Model class for common provider settings like credentials
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | integer<br>string<br>number<br>boolean | | No |
| help | [I18nObject](#i18nobject) | | No |
| label | [I18nObject](#i18nobject) | | No |
| multiple | boolean | | No |
| name | string | The name of the credentials | Yes |
| options | [ [Option](#option) ] | | No |
| placeholder | [I18nObject](#i18nobject) | | No |
| required | boolean | | No |
| scope | [AppSelectorScope](#appselectorscope)<br>[ModelSelectorScope](#modelselectorscope)<br>[ToolSelectorScope](#toolselectorscope) | | No |
| type | [Type](#type) | The type of the credentials | Yes |
| url | string | | No |
#### ProviderCredentialResponse
| Name | Type | Description | Required |
@ -19841,6 +19915,12 @@ Enum class for tool provider
| ---- | ---- | ----------- | -------- |
| ToolProviderType | string | Enum class for tool provider | |
#### ToolSelectorScope
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| ToolSelectorScope | string | | |
#### TraceAppConfigResponse
| Name | Type | Description | Required |

View File

@ -1046,12 +1046,12 @@ Execute a single datasource node within the knowledge pipeline. Returns a stream
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Streaming response with node execution events. | **text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 401 | Unauthorized - invalid API token | |
| 403 | Forbidden - dataset API access or workspace access denied | |
| 404 | `not_found` : Dataset not found. | |
| Code | Description |
| ---- | ----------- |
| 200 | Streaming response with node execution events. |
| 401 | Unauthorized - invalid API token |
| 403 | Forbidden - dataset API access or workspace access denied |
| 404 | `not_found` : Dataset not found. |
### [POST] /datasets/{dataset_id}/pipeline/run
**Run Pipeline**
@ -1072,13 +1072,13 @@ Execute the full knowledge pipeline for a knowledge base. Supports both streamin
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Pipeline execution result. Format depends on `response_mode`: streaming returns a `text/event-stream`, blocking returns a JSON object. | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br>**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 401 | Unauthorized - invalid API token | |
| 403 | `forbidden` : Forbidden. | |
| 404 | `not_found` : Dataset not found. | |
| 500 | `pipeline_run_error` : Pipeline execution failed. | |
| Code | Description |
| ---- | ----------- |
| 200 | Pipeline execution result. Format depends on `response_mode`: streaming returns a `text/event-stream`, blocking returns a JSON object. |
| 401 | Unauthorized - invalid API token |
| 403 | `forbidden` : Forbidden. |
| 404 | `not_found` : Dataset not found. |
| 500 | `pipeline_run_error` : Pipeline execution failed. |
---
## default
@ -2960,7 +2960,7 @@ Enum class for custom configuration status.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| credentials | [ [DatasourceCredentialInfoResponse](#datasourcecredentialinforesponse) ] | | Yes |
| credentials | [ [DatasourceCredentialInfoResponse](#datasourcecredentialinforesponse) ] | | No |
| datasource_type | string | | No |
| node_id | string | | No |
| plugin_id | string | | No |

View File

@ -607,7 +607,11 @@ class TestMiscApis:
method = unwrap(api.get)
service = MagicMock()
service.get_recommended_plugins.return_value = [{"id": "p1"}]
recommended_plugins = {
"installed_recommended_plugins": [{"id": "p1"}],
"uninstalled_recommended_plugins": [{"id": "p2"}],
}
service.get_recommended_plugins.return_value = recommended_plugins
user = make_account()
tenant_id = "tenant-1"
@ -619,7 +623,7 @@ class TestMiscApis:
),
):
result = method(api, tenant_id, user)
assert result == [{"id": "p1"}]
assert result == recommended_plugins
service.get_recommended_plugins.assert_called_once_with("all", user, tenant_id)

View File

@ -1,4 +1,5 @@
import inspect
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
@ -23,6 +24,76 @@ from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from services.datasource_provider_service import DatasourceProviderService
from services.plugin.oauth_service import OAuthProxyService
_PROVIDER_ID = "langgenius/notion_datasource/notion"
def _i18n(text: str) -> dict[str, str]:
return {"en_US": text, "zh_Hans": text, "pt_BR": text, "ja_JP": text}
def _provider_config(name: str, type_: str, label: str, *, required: bool = True) -> dict:
return {
"type": type_,
"name": name,
"scope": None,
"required": required,
"default": None,
"options": None,
"multiple": False,
"label": _i18n(label),
"help": None,
"url": None,
"placeholder": None,
}
def _datasource_credential(credential_id: str = "cred-1", *, is_default: bool = True) -> dict:
return {
"credential": {
"api_key": "******",
"workspace": "engineering",
"database_id": "db-123",
},
"type": "api-key",
"name": "API Key",
"avatar_url": "https://cdn.example.com/notion.png",
"id": credential_id,
"is_default": is_default,
}
def _datasource_auth() -> dict:
return {
"author": "Dify",
"provider": "notion",
"plugin_id": "langgenius/notion_datasource",
"plugin_unique_identifier": "langgenius/notion_datasource:0.0.1",
"icon": "icon.svg",
"name": "notion",
"label": _i18n("Notion"),
"description": _i18n("Notion datasource"),
"credential_schema": [
_provider_config("api_key", "secret-input", "API key"),
],
"oauth_schema": {
"client_schema": [
_provider_config("client_id", "text-input", "Client ID"),
],
"credentials_schema": [
_provider_config("access_token", "secret-input", "Access token"),
],
"oauth_custom_client_params": {"client_id": "masked-client", "client_secret": "********"},
"is_oauth_custom_client_enabled": True,
"is_system_oauth_params_exists": True,
"redirect_uri": "https://api.example.com/oauth/callback",
},
"credentials_list": [_datasource_credential(), _datasource_credential("cred-2", is_default=False)],
}
def _success_response() -> dict[str, str]:
return {"result": "success"}
class TestDatasourcePluginOAuthAuthorizationUrl:
def test_get_success(self, app: Flask):
@ -30,28 +101,50 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
method = inspect.unwrap(api.get)
user = MagicMock(id="user-1")
oauth_client = {"client_id": "abc", "client_secret": "shh", "scopes": ["read", "write"]}
auth_url_payload = {
"authorization_url": "https://auth.example.com/oauth?client_id=abc&state=xyz",
}
with (
app.test_request_context("/?credential_id=cred-1"),
patch.object(
DatasourceProviderService,
"get_oauth_client",
return_value={"client_id": "abc"},
),
return_value=oauth_client,
) as get_oauth_client,
patch.object(
OAuthProxyService,
"create_proxy_context",
return_value="ctx-1",
),
) as create_proxy_context,
patch.object(
OAuthHandler,
"get_authorization_url",
return_value={"url": "http://auth"},
),
return_value=auth_url_payload,
) as get_authorization_url,
):
response = method(api, "tenant-1", user, "notion")
response = method(api, "tenant-1", user, _PROVIDER_ID)
assert response.status_code == 200
assert response.get_json() == auth_url_payload
assert "context_id=ctx-1" in response.headers.get("Set-Cookie")
provider_id = get_oauth_client.call_args.kwargs["datasource_provider_id"]
assert str(provider_id) == _PROVIDER_ID
get_oauth_client.assert_called_once()
create_proxy_context.assert_called_once_with(
user_id="user-1",
tenant_id="tenant-1",
plugin_id="langgenius/notion_datasource",
provider="notion",
credential_id="cred-1",
)
get_authorization_url.assert_called_once()
assert get_authorization_url.call_args.kwargs["tenant_id"] == "tenant-1"
assert get_authorization_url.call_args.kwargs["user_id"] == "user-1"
assert get_authorization_url.call_args.kwargs["plugin_id"] == "langgenius/notion_datasource"
assert get_authorization_url.call_args.kwargs["provider"] == "notion"
assert get_authorization_url.call_args.kwargs["system_credentials"] == oauth_client
def test_get_no_oauth_config(self, app: Flask):
api = DatasourcePluginOAuthAuthorizationUrl()
@ -90,10 +183,10 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
patch.object(
OAuthHandler,
"get_authorization_url",
return_value={"url": "http://auth"},
return_value={"authorization_url": "http://auth"},
),
):
response = method(api, "tenant-1", user, "notion")
response = method(api, "tenant-1", user, _PROVIDER_ID)
assert response.status_code == 200
assert "context_id" in response.headers.get("Set-Cookie")
@ -106,8 +199,9 @@ class TestDatasourceOAuthCallback:
oauth_response = MagicMock()
oauth_response.credentials = {"token": "abc"}
oauth_response.expires_at = None
oauth_response.metadata = {"name": "test"}
expires_at = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC)
oauth_response.expires_at = expires_at
oauth_response.metadata = {"name": "Workspace Bot", "avatar_url": "https://avatar.example.com/bot.png"}
context = {
"user_id": "user-1",
@ -125,7 +219,7 @@ class TestDatasourceOAuthCallback:
patch.object(
DatasourceProviderService,
"get_oauth_client",
return_value={"client_id": "abc"},
return_value={"client_id": "abc", "client_secret": "secret"},
),
patch.object(
OAuthHandler,
@ -136,11 +230,22 @@ class TestDatasourceOAuthCallback:
DatasourceProviderService,
"add_datasource_oauth_provider",
return_value=None,
),
) as add_oauth_provider,
):
response = method(api, "notion")
response = method(api, _PROVIDER_ID)
assert response.status_code == 302
assert "/oauth-callback" in response.location
add_oauth_provider.assert_called_once()
assert add_oauth_provider.call_args.kwargs == {
"tenant_id": "tenant-1",
"provider_id": add_oauth_provider.call_args.kwargs["provider_id"],
"avatar_url": "https://avatar.example.com/bot.png",
"name": "Workspace Bot",
"expire_at": expires_at,
"credentials": {"token": "abc"},
}
assert str(add_oauth_provider.call_args.kwargs["provider_id"]) == _PROVIDER_ID
def test_callback_missing_context(self, app: Flask):
api = DatasourceOAuthCallback()
@ -223,12 +328,16 @@ class TestDatasourceOAuthCallback:
DatasourceProviderService,
"reauthorize_datasource_oauth_provider",
return_value=None,
),
) as reauthorize_provider,
):
response = method(api, "notion")
response = method(api, _PROVIDER_ID)
assert response.status_code == 302
assert "/oauth-callback" in response.location
reauthorize_provider.assert_called_once()
assert str(reauthorize_provider.call_args.kwargs["provider_id"]) == _PROVIDER_ID
assert reauthorize_provider.call_args.kwargs["credential_id"] == "cred-1"
assert reauthorize_provider.call_args.kwargs["credentials"] == {"token": "abc"}
def test_callback_context_id_from_cookie(self, app: Flask):
api = DatasourceOAuthCallback()
@ -278,7 +387,14 @@ class TestDatasourceAuth:
api = DatasourceAuth()
method = inspect.unwrap(api.post)
payload = {"credentials": {"key": "val"}}
payload = {
"name": "Engineering Notion",
"credentials": {
"api_key": "secret-token",
"workspace": "engineering",
"database_id": "db-123",
},
}
with (
app.test_request_context("/", json=payload),
@ -287,11 +403,17 @@ class TestDatasourceAuth:
DatasourceProviderService,
"add_datasource_api_key_provider",
return_value=None,
),
) as add_api_key_provider,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
add_api_key_provider.assert_called_once()
assert add_api_key_provider.call_args.kwargs["tenant_id"] == "tenant-1"
assert str(add_api_key_provider.call_args.kwargs["provider_id"]) == _PROVIDER_ID
assert add_api_key_provider.call_args.kwargs["credentials"] == payload["credentials"]
assert add_api_key_provider.call_args.kwargs["name"] == "Engineering Notion"
def test_post_invalid_credentials(self, app: Flask):
api = DatasourceAuth()
@ -321,19 +443,19 @@ class TestDatasourceAuth:
patch.object(
DatasourceProviderService,
"list_datasource_credentials",
return_value=[{"id": "1"}],
return_value=[_datasource_credential()],
),
):
response, status = method(api, "tenant-1", user, "notion")
response, status = method(api, "tenant-1", user, _PROVIDER_ID)
assert status == 200
assert response["result"]
assert response == {"result": [_datasource_credential()]}
def test_post_missing_credentials(self, app: Flask):
api = DatasourceAuth()
method = inspect.unwrap(api.post)
payload = {}
payload: dict[str, object] = {}
with (
app.test_request_context("/", json=payload),
@ -375,17 +497,24 @@ class TestDatasourceAuthDeleteApi:
DatasourceProviderService,
"remove_datasource_credentials",
return_value=None,
),
) as remove_datasource_credentials,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
remove_datasource_credentials.assert_called_once_with(
tenant_id="tenant-1",
auth_id="cred-1",
provider="notion",
plugin_id="langgenius/notion_datasource",
)
def test_delete_missing_credential_id(self, app: Flask):
api = DatasourceAuthDeleteApi()
method = inspect.unwrap(api.post)
payload = {}
payload: dict[str, object] = {}
with (
app.test_request_context("/", json=payload),
@ -400,7 +529,11 @@ class TestDatasourceAuthUpdateApi:
api = DatasourceAuthUpdateApi()
method = inspect.unwrap(api.post)
payload = {"credential_id": "id", "credentials": {"k": "v"}}
payload = {
"credential_id": "cred-1",
"name": "Updated Notion",
"credentials": {"api_key": "new-secret", "database_id": "db-456"},
}
with (
app.test_request_context("/", json=payload),
@ -409,11 +542,20 @@ class TestDatasourceAuthUpdateApi:
DatasourceProviderService,
"update_datasource_credentials",
return_value=None,
),
) as update_datasource_credentials,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 201
update_datasource_credentials.assert_called_once_with(
tenant_id="tenant-1",
auth_id="cred-1",
provider="notion",
plugin_id="langgenius/notion_datasource",
credentials=payload["credentials"],
name="Updated Notion",
)
def test_update_with_credentials_none(self, app: Flask):
api = DatasourceAuthUpdateApi()
@ -432,7 +574,9 @@ class TestDatasourceAuthUpdateApi:
):
response, status = method(api, "tenant-1", "notion")
assert response == _success_response()
update_mock.assert_called_once()
assert update_mock.call_args.kwargs["credentials"] == {}
assert status == 201
def test_update_name_only(self, app: Flask):
@ -450,8 +594,9 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
),
):
_, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", "notion")
assert response == _success_response()
assert status == 201
def test_update_with_empty_credentials_dict(self, app: Flask):
@ -469,8 +614,9 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_mock,
):
_, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", "notion")
assert response == _success_response()
update_mock.assert_called_once()
assert status == 201
@ -485,12 +631,14 @@ class TestDatasourceAuthListApi:
patch.object(
DatasourceProviderService,
"get_all_datasource_credentials",
return_value=[{"id": "1"}],
return_value=[_datasource_auth()],
),
):
response, status = method(api, "tenant-1")
assert status == 200
assert response == {"result": [_datasource_auth()]}
assert response == {"result": [_datasource_auth()]}
def test_auth_list_empty(self, app: Flask):
api = DatasourceAuthListApi()
@ -537,7 +685,7 @@ class TestDatasourceHardCodeAuthListApi:
patch.object(
DatasourceProviderService,
"get_hard_code_datasource_credentials",
return_value=[{"id": "1"}],
return_value=[_datasource_auth()],
),
):
response, status = method(api, "tenant-1")
@ -550,7 +698,14 @@ class TestDatasourceAuthOauthCustomClient:
api = DatasourceAuthOauthCustomClient()
method = inspect.unwrap(api.post)
payload = {"client_params": {}, "enable_oauth_custom_client": True}
payload = {
"client_params": {
"client_id": "custom-client",
"client_secret": "custom-secret",
"authorize_url": "https://auth.example.com/authorize",
},
"enable_oauth_custom_client": True,
}
with (
app.test_request_context("/", json=payload),
@ -559,11 +714,17 @@ class TestDatasourceAuthOauthCustomClient:
DatasourceProviderService,
"setup_oauth_custom_client_params",
return_value=None,
),
) as setup_custom_client,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
setup_custom_client.assert_called_once()
assert setup_custom_client.call_args.kwargs["tenant_id"] == "tenant-1"
assert str(setup_custom_client.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
assert setup_custom_client.call_args.kwargs["client_params"] == payload["client_params"]
assert setup_custom_client.call_args.kwargs["enabled"] is True
def test_delete_success(self, app: Flask):
api = DatasourceAuthOauthCustomClient()
@ -575,17 +736,20 @@ class TestDatasourceAuthOauthCustomClient:
DatasourceProviderService,
"remove_oauth_custom_client_params",
return_value=None,
),
) as remove_custom_client,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
remove_custom_client.assert_called_once()
assert str(remove_custom_client.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
def test_post_empty_payload(self, app: Flask):
api = DatasourceAuthOauthCustomClient()
method = inspect.unwrap(api.post)
payload = {}
payload: dict[str, object] = {}
with (
app.test_request_context("/", json=payload),
@ -596,8 +760,9 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
),
):
_, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", "notion")
assert response == _success_response()
assert status == 200
def test_post_disabled_flag(self, app: Flask):
@ -618,9 +783,12 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
) as setup_mock,
):
_, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", "notion")
assert response == _success_response()
setup_mock.assert_called_once()
assert setup_mock.call_args.kwargs["client_params"] == {"a": 1}
assert setup_mock.call_args.kwargs["enabled"] is False
assert status == 200
@ -638,17 +806,22 @@ class TestDatasourceAuthDefaultApi:
DatasourceProviderService,
"set_default_datasource_provider",
return_value=None,
),
) as set_default_datasource_provider,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
set_default_datasource_provider.assert_called_once()
assert set_default_datasource_provider.call_args.kwargs["tenant_id"] == "tenant-1"
assert str(set_default_datasource_provider.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
assert set_default_datasource_provider.call_args.kwargs["credential_id"] == "cred-1"
def test_default_missing_id(self, app: Flask):
api = DatasourceAuthDefaultApi()
method = inspect.unwrap(api.post)
payload = {}
payload: dict[str, object] = {}
with (
app.test_request_context("/", json=payload),
@ -663,7 +836,7 @@ class TestDatasourceUpdateProviderNameApi:
api = DatasourceUpdateProviderNameApi()
method = inspect.unwrap(api.post)
payload = {"credential_id": "id", "name": "New Name"}
payload = {"credential_id": "cred-1", "name": "New Name"}
with (
app.test_request_context("/", json=payload),
@ -672,11 +845,17 @@ class TestDatasourceUpdateProviderNameApi:
DatasourceProviderService,
"update_datasource_provider_name",
return_value=None,
),
) as update_datasource_provider_name,
):
response, status = method(api, "tenant-1", "notion")
response, status = method(api, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
update_datasource_provider_name.assert_called_once()
assert update_datasource_provider_name.call_args.kwargs["tenant_id"] == "tenant-1"
assert str(update_datasource_provider_name.call_args.kwargs["datasource_provider_id"]) == _PROVIDER_ID
assert update_datasource_provider_name.call_args.kwargs["name"] == "New Name"
assert update_datasource_provider_name.call_args.kwargs["credential_id"] == "cred-1"
def test_update_name_too_long(self, app: Flask):
api = DatasourceUpdateProviderNameApi()

View File

@ -158,3 +158,65 @@ def test_rag_pipeline_workflow_patch_serializes_response_model(app: Flask, monke
assert response["id"] == "workflow-1"
assert response["marked_name"] == "Updated release"
assert response["hash"] == "hash-1"
def test_default_rag_pipeline_block_configs_serializes_root_response(monkeypatch: pytest.MonkeyPatch) -> None:
block_configs = [{"type": "start", "config": {"title": "Start"}}]
monkeypatch.setattr(
module,
"RagPipelineService",
lambda: SimpleNamespace(get_default_block_configs=lambda: block_configs),
)
api = module.DefaultRagPipelineBlockConfigsApi()
handler = unwrap_all(api.get)
response = handler(api, _pipeline())
assert response == block_configs
def test_draft_rag_pipeline_second_step_parameters_serializes_variables(app, monkeypatch: pytest.MonkeyPatch) -> None:
variables = [
{
"belong_to_node_id": "shared",
"type": "number",
"label": "Chunk size",
"variable": "chunk_size",
"default_value": 1024,
"required": True,
}
]
monkeypatch.setattr(
module,
"RagPipelineService",
lambda: SimpleNamespace(get_second_step_parameters=lambda **_kwargs: variables),
)
api = module.DraftRagPipelineSecondStepApi()
handler = unwrap_all(api.get)
with app.test_request_context("/?node_id=node-1"):
response = handler(api, _pipeline())
assert response["variables"] == variables
def test_rag_pipeline_recommended_plugins_serializes_known_envelope(app, monkeypatch: pytest.MonkeyPatch) -> None:
recommended_plugins = {
"installed_recommended_plugins": [{"name": "Dify Extractor", "meta": {"version": "1.0.0"}}],
"uninstalled_recommended_plugins": [{"plugin_id": "langgenius/notion_datasource"}],
}
monkeypatch.setattr(
module,
"RagPipelineService",
lambda: SimpleNamespace(get_recommended_plugins=lambda *_args: recommended_plugins),
)
api = module.RagPipelineRecommendedPluginApi()
handler = unwrap_all(api.get)
with app.test_request_context("/?type=tool"):
response = handler(api, "tenant-1", _account())
assert response == recommended_plugins

View File

@ -325,10 +325,12 @@ class TestPipelineRunApiEntity:
def test_entity_missing_required_field(self):
"""Test entity raises on missing required field."""
with pytest.raises(ValueError):
PipelineRunApiEntity(
inputs={},
datasource_type="online_document",
# missing datasource_info_list, start_node_id, etc.
PipelineRunApiEntity.model_validate(
{
"inputs": {},
"datasource_type": "online_document",
# missing datasource_info_list, start_node_id, etc.
}
)
@ -382,8 +384,19 @@ class TestDatasourcePluginsApiGet:
mock_dataset = Mock()
mock_db.session.scalar.return_value = mock_dataset
datasource_plugins = [
{
"node_id": "node-datasource-1",
"plugin_id": "plugin-a",
"provider_name": "provider-a",
"datasource_type": "online_document",
"title": "Online Docs",
"user_input_variables": [{"variable": "url", "label": "URL", "type": "text-input", "required": True}],
"credentials": [{"id": "cred-1", "name": "Default credential", "type": "oauth2", "is_default": True}],
}
]
mock_svc_instance = Mock()
mock_svc_instance.get_datasource_plugins.return_value = [{"name": "plugin_a"}]
mock_svc_instance.get_datasource_plugins.return_value = datasource_plugins
mock_svc_cls.return_value = mock_svc_instance
with app.test_request_context("/datasets/test/pipeline/datasource-plugins?is_published=true"):
@ -391,11 +404,33 @@ class TestDatasourcePluginsApiGet:
response, status = api.get(tenant_id=tenant_id, dataset_id=dataset_id)
assert status == 200
assert response == [{"name": "plugin_a"}]
assert response == datasource_plugins
mock_svc_instance.get_datasource_plugins.assert_called_once_with(
tenant_id=tenant_id, dataset_id=dataset_id, is_published=True
)
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.RagPipelineService")
def test_get_plugins_parses_false_is_published_query(self, mock_svc_cls, mock_db, app: Flask):
"""Test false query string is parsed as boolean False."""
tenant_id = str(uuid.uuid4())
dataset_id = str(uuid.uuid4())
mock_db.session.scalar.return_value = Mock()
mock_svc_instance = Mock()
mock_svc_instance.get_datasource_plugins.return_value = []
mock_svc_cls.return_value = mock_svc_instance
with app.test_request_context("/datasets/test/pipeline/datasource-plugins?is_published=false"):
api = DatasourcePluginsApi()
response, status = api.get(tenant_id=tenant_id, dataset_id=dataset_id)
assert status == 200
assert response == []
mock_svc_instance.get_datasource_plugins.assert_called_once_with(
tenant_id=tenant_id, dataset_id=dataset_id, is_published=False
)
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
def test_get_plugins_not_found(self, mock_db, app: Flask):
"""Test NotFound when dataset check fails."""

View File

@ -2,9 +2,10 @@
Unit tests for Service API knowledge pipeline file-upload serialization.
"""
import importlib.util
from datetime import UTC, datetime
from pathlib import Path
from controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow import PipelineUploadFileResponse
from libs.helper import dump_response
class FakeUploadFile:
@ -17,21 +18,7 @@ class FakeUploadFile:
created_at: datetime | None
def _load_serialize_upload_file():
api_dir = Path(__file__).resolve().parents[5]
serializers_path = api_dir / "controllers" / "service_api" / "dataset" / "rag_pipeline" / "serializers.py"
spec = importlib.util.spec_from_file_location("rag_pipeline_serializers", serializers_path)
assert spec
assert spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore[attr-defined]
return module.serialize_upload_file
def test_file_upload_created_at_is_isoformat_string():
serialize_upload_file = _load_serialize_upload_file()
created_at = datetime(2026, 2, 8, 12, 0, 0, tzinfo=UTC)
upload_file = FakeUploadFile()
upload_file.id = "file-1"
@ -42,13 +29,11 @@ def test_file_upload_created_at_is_isoformat_string():
upload_file.created_by = "account-1"
upload_file.created_at = created_at
result = serialize_upload_file(upload_file)
result = dump_response(PipelineUploadFileResponse, upload_file)
assert result["created_at"] == created_at.isoformat()
def test_file_upload_created_at_none_serializes_to_null():
serialize_upload_file = _load_serialize_upload_file()
upload_file = FakeUploadFile()
upload_file.id = "file-1"
upload_file.name = "test.pdf"
@ -58,5 +43,5 @@ def test_file_upload_created_at_none_serializes_to_null():
upload_file.created_by = "account-1"
upload_file.created_at = None
result = serialize_upload_file(upload_file)
result = dump_response(PipelineUploadFileResponse, upload_file)
assert result["created_at"] is None

View File

@ -269,6 +269,7 @@ export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer: string
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
@ -284,12 +285,11 @@ export type MessageDetailResponse = {
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
metadata?: JsonValue | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
@ -723,7 +723,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
@ -743,8 +742,8 @@ export type ConversationAnnotation = {
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
annotation_id: string
created_at?: number | null
id: string
}
export type HumanInputContent = {

View File

@ -570,7 +570,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
@ -1056,8 +1055,8 @@ export const zConversationAnnotation = z.object({
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
annotation_id: z.string(),
created_at: z.int().nullish(),
id: z.string(),
})
/**
@ -2035,6 +2034,7 @@ export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer: z.string(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
@ -2048,12 +2048,11 @@ export const zMessageDetailResponse = z.object({
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
metadata: zJsonValue.nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})

View File

@ -472,6 +472,7 @@ export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer: string
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
@ -487,12 +488,11 @@ export type MessageDetailResponse = {
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
metadata?: JsonValue | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
@ -1498,7 +1498,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
@ -1518,8 +1517,8 @@ export type ConversationAnnotation = {
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
annotation_id: string
created_at?: number | null
id: string
}
export type HumanInputContent = {

View File

@ -1150,7 +1150,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
@ -1371,8 +1370,8 @@ export const zConversationAnnotation = z.object({
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
annotation_id: z.string(),
created_at: z.int().nullish(),
id: z.string(),
})
/**
@ -3455,6 +3454,7 @@ export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer: z.string(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
@ -3468,12 +3468,11 @@ export const zMessageDetailResponse = z.object({
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
metadata: zJsonValue.nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})

View File

@ -4,8 +4,12 @@ export type ClientOptions = {
baseUrl: `${string}://${string}/console/api` | (string & {})
}
export type DatasourceCredentialsResponse = {
result: unknown
export type DatasourceProviderAuthListResponse = {
result: Array<DatasourceProviderAuthResponse>
}
export type DatasourceCredentialListResponse = {
result: Array<DatasourceCredentialResponse>
}
export type DatasourceCredentialPayload = {
@ -47,6 +51,83 @@ export type DatasourceUpdateNamePayload = {
name: string
}
export type DatasourceProviderAuthResponse = {
author: string
credential_schema: Array<ProviderConfig>
credentials_list: Array<DatasourceCredentialResponse>
description: I18nObject
icon: string
label: I18nObject
name: string
oauth_schema: DatasourceOAuthSchemaResponse | null
plugin_id: string
plugin_unique_identifier: string
provider: string
}
export type DatasourceCredentialResponse = {
avatar_url: string | null
credential: {
[key: string]: unknown
}
id: string
is_default: boolean
name: string
type: string
}
export type ProviderConfig = {
default?: number | string | number | boolean | null
help?: I18nObject | null
label?: I18nObject | null
multiple?: boolean
name: string
options?: Array<Option> | null
placeholder?: I18nObject | null
required?: boolean
scope?: AppSelectorScope | ModelSelectorScope | ToolSelectorScope | null
type: Type
url?: string | null
}
export type I18nObject = {
en_US: string
ja_JP?: string | null
pt_BR?: string | null
zh_Hans?: string | null
}
export type DatasourceOAuthSchemaResponse = {
client_schema: Array<ProviderConfig>
credentials_schema: Array<ProviderConfig>
is_oauth_custom_client_enabled: boolean
is_system_oauth_params_exists: boolean
oauth_custom_client_params: {
[key: string]: unknown
} | null
redirect_uri: string
}
export type Option = {
label: I18nObject
value: string
}
export type AppSelectorScope = 'all' | 'chat' | 'completion' | 'workflow'
export type ModelSelectorScope
= | 'llm'
| 'moderation'
| 'rerank'
| 'speech2text'
| 'text-embedding'
| 'tts'
| 'vision'
export type ToolSelectorScope = 'all' | 'builtin' | 'custom' | 'workflow'
export type Type = 'github' | 'marketplace' | 'package'
export type GetAuthPluginDatasourceDefaultListData = {
body?: never
path?: never
@ -55,7 +136,7 @@ export type GetAuthPluginDatasourceDefaultListData = {
}
export type GetAuthPluginDatasourceDefaultListResponses = {
200: DatasourceCredentialsResponse
200: DatasourceProviderAuthListResponse
}
export type GetAuthPluginDatasourceDefaultListResponse
@ -69,7 +150,7 @@ export type GetAuthPluginDatasourceListData = {
}
export type GetAuthPluginDatasourceListResponses = {
200: DatasourceCredentialsResponse
200: DatasourceProviderAuthListResponse
}
export type GetAuthPluginDatasourceListResponse
@ -85,7 +166,7 @@ export type GetAuthPluginDatasourceByProviderIdData = {
}
export type GetAuthPluginDatasourceByProviderIdResponses = {
200: DatasourceCredentialsResponse
200: DatasourceCredentialListResponse
}
export type GetAuthPluginDatasourceByProviderIdResponse

View File

@ -2,13 +2,6 @@
import * as z from 'zod'
/**
* DatasourceCredentialsResponse
*/
export const zDatasourceCredentialsResponse = z.object({
result: z.unknown(),
})
/**
* DatasourceCredentialPayload
*/
@ -64,23 +57,145 @@ export const zDatasourceUpdateNamePayload = z.object({
})
/**
* Success
* DatasourceCredentialResponse
*/
export const zGetAuthPluginDatasourceDefaultListResponse = zDatasourceCredentialsResponse
export const zDatasourceCredentialResponse = z.object({
avatar_url: z.string().nullable(),
credential: z.record(z.string(), z.unknown()),
id: z.string(),
is_default: z.boolean(),
name: z.string(),
type: z.string(),
})
/**
* Success
* DatasourceCredentialListResponse
*/
export const zGetAuthPluginDatasourceListResponse = zDatasourceCredentialsResponse
export const zDatasourceCredentialListResponse = z.object({
result: z.array(zDatasourceCredentialResponse),
})
/**
* I18nObject
*
* Model class for i18n object.
*/
export const zI18nObject = z.object({
en_US: z.string(),
ja_JP: z.string().nullish(),
pt_BR: z.string().nullish(),
zh_Hans: z.string().nullish(),
})
/**
* Option
*/
export const zOption = z.object({
label: zI18nObject,
value: z.string(),
})
/**
* AppSelectorScope
*/
export const zAppSelectorScope = z.enum(['all', 'chat', 'completion', 'workflow'])
/**
* ModelSelectorScope
*/
export const zModelSelectorScope = z.enum([
'llm',
'moderation',
'rerank',
'speech2text',
'text-embedding',
'tts',
'vision',
])
/**
* ToolSelectorScope
*/
export const zToolSelectorScope = z.enum(['all', 'builtin', 'custom', 'workflow'])
/**
* Type
*/
export const zType = z.enum(['github', 'marketplace', 'package'])
/**
* ProviderConfig
*
* Model class for common provider settings like credentials
*/
export const zProviderConfig = z.object({
default: z.union([z.int(), z.string(), z.number(), z.boolean()]).nullish(),
help: zI18nObject.nullish(),
label: zI18nObject.nullish(),
multiple: z.boolean().optional().default(false),
name: z.string(),
options: z.array(zOption).nullish(),
placeholder: zI18nObject.nullish(),
required: z.boolean().optional().default(false),
scope: z.union([zAppSelectorScope, zModelSelectorScope, zToolSelectorScope]).nullish(),
type: zType,
url: z.string().nullish(),
})
/**
* DatasourceOAuthSchemaResponse
*/
export const zDatasourceOAuthSchemaResponse = z.object({
client_schema: z.array(zProviderConfig),
credentials_schema: z.array(zProviderConfig),
is_oauth_custom_client_enabled: z.boolean(),
is_system_oauth_params_exists: z.boolean(),
oauth_custom_client_params: z.record(z.string(), z.unknown()).nullable(),
redirect_uri: z.string(),
})
/**
* DatasourceProviderAuthResponse
*/
export const zDatasourceProviderAuthResponse = z.object({
author: z.string(),
credential_schema: z.array(zProviderConfig),
credentials_list: z.array(zDatasourceCredentialResponse),
description: zI18nObject,
icon: z.string(),
label: zI18nObject,
name: z.string(),
oauth_schema: zDatasourceOAuthSchemaResponse.nullable(),
plugin_id: z.string(),
plugin_unique_identifier: z.string(),
provider: z.string(),
})
/**
* DatasourceProviderAuthListResponse
*/
export const zDatasourceProviderAuthListResponse = z.object({
result: z.array(zDatasourceProviderAuthResponse),
})
/**
* Default datasource credentials retrieved successfully
*/
export const zGetAuthPluginDatasourceDefaultListResponse = zDatasourceProviderAuthListResponse
/**
* Datasource credentials retrieved successfully
*/
export const zGetAuthPluginDatasourceListResponse = zDatasourceProviderAuthListResponse
export const zGetAuthPluginDatasourceByProviderIdPath = z.object({
provider_id: z.string(),
})
/**
* Success
* Datasource credentials retrieved successfully
*/
export const zGetAuthPluginDatasourceByProviderIdResponse = zDatasourceCredentialsResponse
export const zGetAuthPluginDatasourceByProviderIdResponse = zDatasourceCredentialListResponse
export const zPostAuthPluginDatasourceByProviderIdBody = zDatasourceCredentialPayload
@ -89,7 +204,7 @@ export const zPostAuthPluginDatasourceByProviderIdPath = z.object({
})
/**
* Success
* Datasource credential created successfully
*/
export const zPostAuthPluginDatasourceByProviderIdResponse = zSimpleResultResponse
@ -109,7 +224,7 @@ export const zPostAuthPluginDatasourceByProviderIdCustomClientPath = z.object({
})
/**
* Success
* Datasource OAuth custom client saved successfully
*/
export const zPostAuthPluginDatasourceByProviderIdCustomClientResponse = zSimpleResultResponse
@ -142,7 +257,7 @@ export const zPostAuthPluginDatasourceByProviderIdUpdatePath = z.object({
})
/**
* Success
* Datasource credential updated successfully
*/
export const zPostAuthPluginDatasourceByProviderIdUpdateResponse = zSimpleResultResponse

View File

@ -246,7 +246,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number

View File

@ -266,7 +266,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),

View File

@ -135,7 +135,7 @@ export const zGetOauthPluginByProviderIdDatasourceGetAuthorizationUrlQuery = z.o
})
/**
* Authorization URL retrieved successfully
* Datasource OAuth authorization URL generated successfully
*/
export const zGetOauthPluginByProviderIdDatasourceGetAuthorizationUrlResponse
= zPluginOAuthAuthorizationUrlResponse

View File

@ -113,9 +113,6 @@ import {
zPostRagPipelinesByPipelineIdWorkflowsDraftRunBody,
zPostRagPipelinesByPipelineIdWorkflowsDraftRunPath,
zPostRagPipelinesByPipelineIdWorkflowsDraftRunResponse,
zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewBody,
zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewPath,
zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponse,
zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunBody,
zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunPath,
zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunResponse,
@ -1065,34 +1062,10 @@ export const publish2 = {
post: post16,
}
/**
* Run datasource content preview
*/
export const post17 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreview',
path: '/rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/preview',
summary: 'Run datasource content preview',
tags: ['console'],
})
.input(
z.object({
body: zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewBody,
params: zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewPath,
}),
)
.output(zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponse)
export const preview = {
post: post17,
}
/**
* Run rag pipeline datasource
*/
export const post18 = oc
export const post17 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1110,11 +1083,10 @@ export const post18 = oc
.output(zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunResponse)
export const run6 = {
post: post18,
post: post17,
}
export const byNodeId5 = {
preview,
run: run6,
}
@ -1185,7 +1157,7 @@ export const processing2 = {
/**
* Run published workflow
*/
export const post19 = oc
export const post18 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1203,7 +1175,7 @@ export const post19 = oc
.output(zPostRagPipelinesByPipelineIdWorkflowsPublishedRunResponse)
export const run7 = {
post: post19,
post: post18,
}
export const published = {
@ -1213,7 +1185,7 @@ export const published = {
run: run7,
}
export const post20 = oc
export const post19 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -1225,7 +1197,7 @@ export const post20 = oc
.output(zPostRagPipelinesByPipelineIdWorkflowsByWorkflowIdRestoreResponse)
export const restore = {
post: post20,
post: post19,
}
/**

View File

@ -319,16 +319,6 @@ export type RagPipelineWorkflowPublishResponse = {
result: string
}
export type Parser = {
credential_id?: string | null
datasource_type: string
inputs: {
[key: string]: unknown
}
}
export type DataSourceContentPreviewResponse = unknown
export type PublishedWorkflowRunPayload = {
datasource_info_list: Array<{
[key: string]: unknown
@ -1317,24 +1307,6 @@ export type PostRagPipelinesByPipelineIdWorkflowsPublishResponses = {
export type PostRagPipelinesByPipelineIdWorkflowsPublishResponse
= PostRagPipelinesByPipelineIdWorkflowsPublishResponses[keyof PostRagPipelinesByPipelineIdWorkflowsPublishResponses]
export type PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewData = {
body: Parser
path: {
node_id: string
pipeline_id: string
}
query?: never
url: '/rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/preview'
}
export type PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponses
= {
200: DataSourceContentPreviewResponse
}
export type PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponse
= PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponses[keyof PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponses]
export type PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunData = {
body: DatasourceNodeRunPayload
path: {

View File

@ -190,20 +190,6 @@ export const zRagPipelineWorkflowPublishResponse = z.object({
result: z.string(),
})
/**
* Parser
*/
export const zParser = z.object({
credential_id: z.string().nullish(),
datasource_type: z.string(),
inputs: z.record(z.string(), z.unknown()),
})
/**
* DataSourceContentPreviewResponse
*/
export const zDataSourceContentPreviewResponse = z.unknown()
/**
* PublishedWorkflowRunPayload
*/
@ -1166,21 +1152,6 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishPath = z.object({
export const zPostRagPipelinesByPipelineIdWorkflowsPublishResponse
= zRagPipelineWorkflowPublishResponse
export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewBody
= zParser
export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewPath
= z.object({
node_id: z.string(),
pipeline_id: z.uuid(),
})
/**
* Success
*/
export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponse
= zDataSourceContentPreviewResponse
export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunBody
= zDatasourceNodeRunPayload

View File

@ -567,7 +567,7 @@ export type DatasourceNodeRunPayload = {
export type DatasourcePluginListResponse = Array<DatasourcePluginResponse>
export type DatasourcePluginResponse = {
credentials: Array<DatasourceCredentialInfoResponse>
credentials?: Array<DatasourceCredentialInfoResponse>
datasource_type?: string | null
node_id?: string | null
plugin_id?: string | null
@ -3188,13 +3188,24 @@ export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunData = {
}
export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunErrors = {
401: unknown
403: unknown
404: unknown
401: {
[key: string]: unknown
}
403: {
[key: string]: unknown
}
404: {
[key: string]: unknown
}
}
export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunError
= PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunErrors[keyof PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunErrors]
export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponses = {
200: GeneratedAppResponse
200: {
[key: string]: unknown
}
}
export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponse
@ -3210,14 +3221,27 @@ export type PostDatasetsByDatasetIdPipelineRunData = {
}
export type PostDatasetsByDatasetIdPipelineRunErrors = {
401: unknown
403: unknown
404: unknown
500: unknown
401: {
[key: string]: unknown
}
403: {
[key: string]: unknown
}
404: {
[key: string]: unknown
}
500: {
[key: string]: unknown
}
}
export type PostDatasetsByDatasetIdPipelineRunError
= PostDatasetsByDatasetIdPipelineRunErrors[keyof PostDatasetsByDatasetIdPipelineRunErrors]
export type PostDatasetsByDatasetIdPipelineRunResponses = {
200: GeneratedAppResponse
200: {
[key: string]: unknown
}
}
export type PostDatasetsByDatasetIdPipelineRunResponse

View File

@ -698,7 +698,7 @@ export const zDatasourceNodeRunPayload = z.object({
* DatasourcePluginResponse
*/
export const zDatasourcePluginResponse = z.object({
credentials: z.array(zDatasourceCredentialInfoResponse),
credentials: z.array(zDatasourceCredentialInfoResponse).optional(),
datasource_type: z.string().nullish(),
node_id: z.string().nullish(),
plugin_id: z.string().nullish(),
@ -3061,8 +3061,10 @@ export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunPath = z.
/**
* Streaming response with node execution events.
*/
export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponse
= zGeneratedAppResponse
export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponse = z.record(
z.string(),
z.unknown(),
)
export const zPostDatasetsByDatasetIdPipelineRunBody = zPipelineRunApiEntity
@ -3073,7 +3075,7 @@ export const zPostDatasetsByDatasetIdPipelineRunPath = z.object({
/**
* Pipeline execution result. Format depends on `response_mode`: streaming returns a `text/event-stream`, blocking returns a JSON object.
*/
export const zPostDatasetsByDatasetIdPipelineRunResponse = zGeneratedAppResponse
export const zPostDatasetsByDatasetIdPipelineRunResponse = z.record(z.string(), z.unknown())
export const zPostDatasetsByDatasetIdRetrieveBody = zHitTestingPayload

View File

@ -10,13 +10,21 @@ type SwaggerSchema = JsonObject & {
$ref?: string
}
type OpenApiMediaType = JsonObject & {
schema?: SwaggerSchema
}
type OpenApiResponse = JsonObject & {
content?: Record<string, OpenApiMediaType>
}
type OpenApiComponents = JsonObject & {
schemas?: Record<string, SwaggerSchema>
}
type SwaggerOperation = JsonObject & {
operationId?: string
responses?: Record<string, unknown>
responses?: Record<string, OpenApiResponse>
}
type SwaggerDocument = JsonObject & {
@ -52,6 +60,17 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
const pydanticDecimalStringPattern = '^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$'
const codegenSafeDecimalStringPattern = '^(?![-+.]*$)[+-]?0*\\d*\\.?\\d*$'
const opaqueJsonContent = (): Record<string, OpenApiMediaType> => ({
'application/json': {
schema: {
additionalProperties: true,
type: 'object',
},
},
})
const apiSpecs: ApiSpec[] = [
{ filename: 'console-openapi.json', name: 'console' },
@ -182,6 +201,46 @@ const addOperationIds = (document: SwaggerDocument) => {
}
}
const isOpaqueContractResponse = (response: OpenApiResponse) => {
const content = response.content
if (!isObject(content))
return false
return Object.entries(content).some(([mediaType, media]) => {
if (!isObject(media))
return false
return (mediaType === 'application/json' || mediaType === 'text/event-stream') && !('schema' in media)
})
}
const hasOpaqueContractSuccessResponse = (operation: SwaggerOperation) => {
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
return /^2\d\d$/.test(status) && isObject(response) && isOpaqueContractResponse(response)
})
}
const normalizeOpaqueContractResponses = (document: SwaggerDocument) => {
// Some backend endpoints has no schema (e.g. external) and will trap heyapi here
// So we forge an opaque schema here
for (const pathItem of Object.values(document.paths ?? {})) {
for (const [method, operation] of Object.entries(pathItem)) {
if (!operationMethods.has(method) || !isObject(operation))
continue
const swaggerOperation = operation as SwaggerOperation
if (!hasOpaqueContractSuccessResponse(swaggerOperation))
continue
Object.values(swaggerOperation.responses ?? {})
.filter(response => isObject(response) && isOpaqueContractResponse(response))
.forEach((response) => {
response.content = opaqueJsonContent()
})
}
}
}
const hasSuccessResponse = (operation: SwaggerOperation) => {
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
if (!/^2\d\d$/.test(status))
@ -215,6 +274,7 @@ const filterContractOperations = (document: SwaggerDocument) => {
}
const normalizeApiSwagger = (document: SwaggerDocument) => {
normalizeOpaqueContractResponses(document)
filterContractOperations(document)
addOperationIds(document)
@ -380,10 +440,20 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
'name': 'zod',
'~resolvers': {
string: (ctx) => {
if (ctx.schema.format !== 'binary')
return undefined
if (ctx.schema.format === 'binary')
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
if (ctx.schema.pattern === pydanticDecimalStringPattern) {
// the pydantic generated regex will emit error like
// regexp/no-useless-assertions, so patch the regex here
return $(ctx.symbols.z)
.attr('string')
.call()
.attr('regex')
.call($.regexp(codegenSafeDecimalStringPattern))
}
return undefined
},
},
},

View File

@ -217,14 +217,8 @@ const toFeedback = (feedback: NonNullable<MessageDetailResponse['feedbacks']>[nu
}
}
type AgentDebugMessageWithLegacyAnswer = MessageDetailResponse & {
answer?: string | null
}
const getAgentDebugMessageAnswer = (message: MessageDetailResponse) => {
const legacyAnswer = (message as AgentDebugMessageWithLegacyAnswer).answer
return message.re_sign_file_url_answer ?? legacyAnswer ?? ''
return message.answer ?? ''
}
function getFormattedAgentDebugChatTree(messages: MessageDetailResponse[]): ChatItemInTree[] {