mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 06:41:10 +08:00
Merge bbae131a38 into a246dc8b17
This commit is contained in:
commit
75915b4469
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -135,7 +135,7 @@ export const zGetOauthPluginByProviderIdDatasourceGetAuthorizationUrlQuery = z.o
|
||||
})
|
||||
|
||||
/**
|
||||
* Authorization URL retrieved successfully
|
||||
* Datasource OAuth authorization URL generated successfully
|
||||
*/
|
||||
export const zGetOauthPluginByProviderIdDatasourceGetAuthorizationUrlResponse
|
||||
= zPluginOAuthAuthorizationUrlResponse
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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[] {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user