refactor: fix OpenAPI contract generation schemas (#37387)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-06-12 22:25:53 +08:00 committed by GitHub
parent 7cf75c3cc5
commit 5d77c0af08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
222 changed files with 15425 additions and 10146 deletions

View File

@ -62,7 +62,7 @@ class WorkflowListQuery(BaseModel):
class WorkflowRunPayload(BaseModel):
inputs: dict[str, Any]
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
class WorkflowUpdatePayload(BaseModel):

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, computed_field
from pydantic import BaseModel, ConfigDict, Field, RootModel, computed_field
from fields.base import ResponseModel
from graphon.file import helpers as file_helpers
@ -24,6 +24,34 @@ class SimpleResultResponse(ResponseModel):
result: str
class GeneratedAppResponse(RootModel[JSONValue]):
root: JSONValue
class EventStreamResponse(RootModel[str]):
root: str
class TextFileResponse(RootModel[str]):
root: str
class RedirectResponse(RootModel[str]):
root: str
class BinaryFileResponse(RootModel[bytes]):
root: bytes
class AudioBinaryResponse(RootModel[bytes]):
root: bytes
class AudioTranscriptResponse(ResponseModel):
text: str
class SimpleResultMessageResponse(ResponseModel):
result: str
message: str

View File

@ -1,10 +1,17 @@
from typing import Any
from flask import request
from flask_restx import Resource, fields
from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, query_params_from_model
from controllers.common.schema import (
DEFAULT_REF_TEMPLATE_OPENAPI_3_0,
query_params_from_model,
register_response_schema_models,
)
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, setup_required
from fields.base import ResponseModel
from libs.login import login_required
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
@ -16,10 +23,16 @@ class AdvancedPromptTemplateQuery(BaseModel):
model_name: str = Field(..., description="Model name")
class AdvancedPromptTemplateResponse(ResponseModel):
chat_prompt_config: dict[str, Any] | None = Field(default=None)
completion_prompt_config: dict[str, Any] | None = Field(default=None)
console_ns.schema_model(
AdvancedPromptTemplateQuery.__name__,
AdvancedPromptTemplateQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
)
register_response_schema_models(console_ns, AdvancedPromptTemplateResponse)
@console_ns.route("/app/prompt-templates")
@ -28,7 +41,9 @@ class AdvancedPromptTemplateList(Resource):
@console_ns.doc(description="Get advanced prompt templates based on app mode and model configuration")
@console_ns.doc(params=query_params_from_model(AdvancedPromptTemplateQuery))
@console_ns.response(
200, "Prompt templates retrieved successfully", fields.List(fields.Raw(description="Prompt template data"))
200,
"Prompt templates retrieved successfully",
console_ns.models[AdvancedPromptTemplateResponse.__name__],
)
@console_ns.response(400, "Invalid request parameters")
@setup_required

View File

@ -1,12 +1,15 @@
from flask import request
from flask_restx import Resource, fields
from pydantic import BaseModel, Field, field_validator
from typing import Any
from controllers.common.schema import query_params_from_model, register_schema_models
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel, field_validator
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.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import uuid_value
from libs.login import login_required
from models import Account
@ -28,7 +31,53 @@ class AgentLogQuery(BaseModel):
return uuid_value(value)
class AgentLogMetaResponse(ResponseModel):
status: str
executor: str
start_time: str
elapsed_time: float | None = None
total_tokens: int
agent_mode: str
iterations: int
class AgentToolCallResponse(ResponseModel):
status: str
error: str | None = None
time_cost: float | int
tool_name: str
tool_label: str
tool_input: dict[str, Any]
tool_output: dict[str, Any]
tool_parameters: dict[str, Any]
tool_icon: Any = Field(default=None)
class AgentIterationLogResponse(ResponseModel):
tokens: int
tool_calls: list[AgentToolCallResponse]
tool_raw: dict[str, Any]
thought: str | None = None
created_at: str
files: list[Any] = Field(default_factory=list)
class AgentLogResponse(ResponseModel):
meta: AgentLogMetaResponse
iterations: list[AgentIterationLogResponse]
files: list[Any] = Field(default_factory=list)
class AgentSkillUploadResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class AgentSkillStandardizeResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(console_ns, AgentLogQuery)
register_response_schema_models(console_ns, AgentLogResponse, AgentSkillUploadResponse, AgentSkillStandardizeResponse)
@console_ns.route("/apps/<uuid:app_id>/agent/logs")
@ -37,9 +86,7 @@ class AgentLogApi(Resource):
@console_ns.doc(description="Get agent execution logs for an application")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(AgentLogQuery))
@console_ns.response(
200, "Agent logs retrieved successfully", fields.List(fields.Raw(description="Agent log entries"))
)
@console_ns.response(200, "Agent logs retrieved successfully", console_ns.models[AgentLogResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@setup_required
@login_required
@ -57,7 +104,7 @@ class AgentSkillUploadApi(Resource):
@console_ns.doc("upload_agent_skill")
@console_ns.doc(description="Upload + validate a Skill package (.zip/.skill) and extract its manifest")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(201, "Skill validated")
@console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__])
@console_ns.response(400, "Invalid skill package")
@setup_required
@login_required
@ -97,7 +144,11 @@ class AgentSkillStandardizeApi(Resource):
@console_ns.doc("standardize_agent_skill")
@console_ns.doc(description="Validate + standardize a Skill into the agent drive (ENG-594)")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(201, "Skill standardized into drive")
@console_ns.response(
201,
"Skill standardized into drive",
console_ns.models[AgentSkillStandardizeResponse.__name__],
)
@console_ns.response(400, "Invalid skill package or no bound agent")
@setup_required
@login_required

View File

@ -6,7 +6,7 @@ from flask_restx import Resource
from pydantic import BaseModel, Field, TypeAdapter, field_validator
from controllers.common.errors import NoFileUploadedError, TooManyFilesError
from controllers.common.schema import query_params_from_model, register_schema_models
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 (
account_initialization_required,
@ -24,6 +24,7 @@ from fields.annotation_fields import (
AnnotationHitHistoryList,
AnnotationList,
)
from fields.base import ResponseModel
from libs.helper import uuid_value
from libs.login import login_required
from services.annotation_service import (
@ -56,7 +57,10 @@ class CreateAnnotationPayload(BaseModel):
question: str | None = Field(default=None, description="Question text")
answer: str | None = Field(default=None, description="Answer text")
content: str | None = Field(default=None, description="Content text")
annotation_reply: dict[str, Any] | None = Field(default=None, description="Annotation reply data")
annotation_reply: dict[str, Any] | None = Field(
default=None,
description="Annotation reply data",
)
@field_validator("message_id")
@classmethod
@ -70,7 +74,7 @@ class UpdateAnnotationPayload(BaseModel):
question: str | None = None
answer: str | None = None
content: str | None = None
annotation_reply: dict[str, Any] | None = None
annotation_reply: dict[str, Any] | None = Field(default=None)
class AnnotationReplyStatusQuery(BaseModel):
@ -91,6 +95,25 @@ class AnnotationFilePayload(BaseModel):
return uuid_value(value)
class AnnotationJobStatusResponse(ResponseModel):
job_id: str | None = None
job_status: str | None = None
error_msg: str | None = None
record_count: int | None = None
class AnnotationEmbeddingModelResponse(ResponseModel):
embedding_provider_name: str | None = None
embedding_model_name: str | None = None
class AnnotationSettingResponse(ResponseModel):
id: str | None = None
enabled: bool
score_threshold: float | None = None
embedding_model: AnnotationEmbeddingModelResponse | None = None
register_schema_models(
console_ns,
Annotation,
@ -107,6 +130,16 @@ register_schema_models(
AnnotationHitHistoryListQuery,
AnnotationFilePayload,
)
register_response_schema_models(
console_ns,
Annotation,
AnnotationList,
AnnotationExportList,
AnnotationHitHistory,
AnnotationHitHistoryList,
AnnotationJobStatusResponse,
AnnotationSettingResponse,
)
@console_ns.route("/apps/<uuid:app_id>/annotation-reply/<string:action>")
@ -115,7 +148,7 @@ class AnnotationReplyActionApi(Resource):
@console_ns.doc(description="Enable or disable annotation reply for an app")
@console_ns.doc(params={"app_id": "Application ID", "action": "Action to perform (enable/disable)"})
@console_ns.expect(console_ns.models[AnnotationReplyPayload.__name__])
@console_ns.response(200, "Action completed successfully")
@console_ns.response(200, "Action completed successfully", console_ns.models[AnnotationJobStatusResponse.__name__])
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@ -142,7 +175,11 @@ class AppAnnotationSettingDetailApi(Resource):
@console_ns.doc("get_annotation_setting")
@console_ns.doc(description="Get annotation settings for an app")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Annotation settings retrieved successfully")
@console_ns.response(
200,
"Annotation settings retrieved successfully",
console_ns.models[AnnotationSettingResponse.__name__],
)
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@ -159,7 +196,7 @@ class AppAnnotationSettingUpdateApi(Resource):
@console_ns.doc(description="Update annotation settings for an app")
@console_ns.doc(params={"app_id": "Application ID", "annotation_setting_id": "Annotation setting ID"})
@console_ns.expect(console_ns.models[AnnotationSettingUpdatePayload.__name__])
@console_ns.response(200, "Settings updated successfully")
@console_ns.response(200, "Settings updated successfully", console_ns.models[AnnotationSettingResponse.__name__])
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@ -182,7 +219,11 @@ class AnnotationReplyActionStatusApi(Resource):
@console_ns.doc("get_annotation_reply_action_status")
@console_ns.doc(description="Get status of annotation reply action job")
@console_ns.doc(params={"app_id": "Application ID", "job_id": "Job ID", "action": "Action type"})
@console_ns.response(200, "Job status retrieved successfully")
@console_ns.response(
200,
"Job status retrieved successfully",
console_ns.models[AnnotationJobStatusResponse.__name__],
)
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@ -211,7 +252,7 @@ class AnnotationApi(Resource):
@console_ns.doc(description="Get annotations for an app with pagination")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(AnnotationListQuery))
@console_ns.response(200, "Annotations retrieved successfully")
@console_ns.response(200, "Annotations retrieved successfully", console_ns.models[AnnotationList.__name__])
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@ -263,6 +304,7 @@ class AnnotationApi(Resource):
@login_required
@account_initialization_required
@edit_permission_required
@console_ns.response(204, "Annotations deleted successfully")
def delete(self, app_id: UUID):
# Use request.args.getlist to get annotation_ids array directly
@ -341,6 +383,7 @@ class AnnotationUpdateDeleteApi(Resource):
@login_required
@account_initialization_required
@edit_permission_required
@console_ns.response(204, "Annotation deleted successfully")
def delete(self, app_id: UUID, annotation_id: UUID):
AppAnnotationService.delete_app_annotation(str(app_id), str(annotation_id))
return "", 204
@ -351,7 +394,11 @@ class AnnotationBatchImportApi(Resource):
@console_ns.doc("batch_import_annotations")
@console_ns.doc(description="Batch import annotations from CSV file with rate limiting and security checks")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Batch import started successfully")
@console_ns.response(
200,
"Batch import started successfully",
console_ns.models[AnnotationJobStatusResponse.__name__],
)
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "No file uploaded or too many files")
@console_ns.response(413, "File too large")
@ -404,7 +451,11 @@ class AnnotationBatchImportStatusApi(Resource):
@console_ns.doc("get_batch_import_status")
@console_ns.doc(description="Get status of batch import job")
@console_ns.doc(params={"app_id": "Application ID", "job_id": "Job ID"})
@console_ns.response(200, "Job status retrieved successfully")
@console_ns.response(
200,
"Job status retrieved successfully",
console_ns.models[AnnotationJobStatusResponse.__name__],
)
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required

View File

@ -211,6 +211,11 @@ class AppTracePayload(BaseModel):
return value
class AppTraceResponse(ResponseModel):
enabled: bool
tracing_provider: str | None = None
type JSONValue = Any
@ -452,7 +457,7 @@ class AppExportResponse(ResponseModel):
register_enum_models(console_ns, RetrievalMethod, WorkflowExecutionStatus, DatasetPermissionEnum)
register_response_schema_models(console_ns, RedirectUrlResponse, SimpleResultResponse)
register_response_schema_models(console_ns, AppTraceResponse, RedirectUrlResponse, SimpleResultResponse)
register_schema_models(
console_ns,
@ -817,7 +822,7 @@ class AppIconApi(Resource):
@console_ns.doc(description="Update application icon")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[AppIconPayload.__name__])
@console_ns.response(200, "Icon updated successfully")
@console_ns.response(200, "Icon updated successfully", console_ns.models[AppDetail.__name__])
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@ -887,7 +892,11 @@ class AppTraceApi(Resource):
@console_ns.doc("get_app_trace")
@console_ns.doc(description="Get app tracing configuration")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Trace configuration retrieved successfully")
@console_ns.response(
200,
"Trace configuration retrieved successfully",
console_ns.models[AppTraceResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required

View File

@ -1,12 +1,14 @@
import logging
from typing import Any
from flask import request
from flask_restx import Resource, fields
from pydantic import BaseModel, Field
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel
from werkzeug.exceptions import InternalServerError
import services
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.common.fields import AudioBinaryResponse
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.app.error import (
AppUnavailableError,
@ -51,7 +53,12 @@ class AudioTranscriptResponse(BaseModel):
text: str = Field(description="Transcribed text from audio")
class TextToSpeechVoiceListResponse(RootModel[list[dict[str, Any]]]):
root: list[dict[str, Any]]
register_schema_models(console_ns, AudioTranscriptResponse, TextToSpeechPayload, TextToSpeechVoiceQuery)
register_response_schema_models(console_ns, AudioBinaryResponse, TextToSpeechVoiceListResponse)
@console_ns.route("/apps/<uuid:app_id>/audio-to-text")
@ -113,7 +120,11 @@ class ChatMessageTextApi(Resource):
@console_ns.doc(description="Convert text to speech for chat messages")
@console_ns.doc(params={"app_id": "App ID"})
@console_ns.expect(console_ns.models[TextToSpeechPayload.__name__])
@console_ns.response(200, "Text to speech conversion successful")
@console_ns.response(
200,
"Text to speech conversion successful",
console_ns.models[AudioBinaryResponse.__name__],
)
@console_ns.response(400, "Bad request - Invalid parameters")
@get_app_model
@setup_required
@ -164,7 +175,9 @@ class TextModesApi(Resource):
@console_ns.doc(params={"app_id": "App ID"})
@console_ns.doc(params=query_params_from_model(TextToSpeechVoiceQuery))
@console_ns.response(
200, "TTS voices retrieved successfully", fields.List(fields.Raw(description="Available voices"))
200,
"TTS voices retrieved successfully",
console_ns.models[TextToSpeechVoiceListResponse.__name__],
)
@console_ns.response(400, "Invalid language parameter")
@get_app_model

View File

@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.app.error import (
@ -64,8 +64,14 @@ class BaseMessagePayload(BaseModel):
# Soul, so no override ``model_config`` is sent; chat / agent-chat / completion
# debugging still pass it. Optional here, required in practice by those modes
# downstream when their config is built from args.
model_config_data: dict[str, Any] = Field(default_factory=dict, alias="model_config")
files: list[Any] | None = Field(default=None, description="Uploaded files")
model_config_data: dict[str, Any] = Field(
default_factory=dict,
alias="model_config",
)
files: list[Any] | None = Field(
default=None,
description="Uploaded files",
)
response_mode: Literal["blocking", "streaming"] = Field(default="blocking", description="Response mode")
retriever_from: str = Field(default="dev", description="Retriever source")
@ -88,7 +94,7 @@ class ChatMessagePayload(BaseMessagePayload):
register_schema_models(console_ns, CompletionMessagePayload, ChatMessagePayload)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(console_ns, GeneratedAppResponse, SimpleResultResponse)
# define completion message api for user
@ -98,7 +104,7 @@ class CompletionMessageApi(Resource):
@console_ns.doc(description="Generate completion message for debugging")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[CompletionMessagePayload.__name__])
@console_ns.response(200, "Completion generated successfully")
@console_ns.response(200, "Completion generated successfully", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(404, "App not found")
@setup_required
@ -170,7 +176,7 @@ class ChatMessageApi(Resource):
@console_ns.doc(description="Generate chat message for debugging")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[ChatMessagePayload.__name__])
@console_ns.response(200, "Chat message generated successfully")
@console_ns.response(200, "Chat message generated successfully", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(404, "App or conversation not found")
@setup_required

View File

@ -1,11 +1,12 @@
from collections.abc import Sequence
from typing import Literal
from typing import Any, Literal
from flask_restx import Resource
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel
from sqlalchemy.orm import Session
from controllers.common.schema import register_enum_models, register_schema_models
from controllers.common.fields import SimpleDataResponse
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.app.error import (
CompletionRequestError,
@ -36,7 +37,11 @@ class InstructionGeneratePayload(BaseModel):
current: str = Field(default="", description="Current instruction text")
language: str = Field(default="javascript", description="Programming language (javascript/python)")
instruction: str = Field(..., description="Instruction for generation")
model_config_data: ModelConfig = Field(..., alias="model_config", description="Model configuration")
model_config_data: ModelConfig = Field(
...,
alias="model_config",
description="Model configuration",
)
ideal_output: str = Field(default="", description="Expected ideal output")
@ -62,13 +67,21 @@ class WorkflowGeneratePayload(BaseModel):
mode: Literal["workflow", "advanced-chat"] = Field(..., description="Target app mode for the generated graph")
instruction: str = Field(..., description="Natural-language workflow description")
ideal_output: str = Field(default="", description="Optional sample output for grounding")
model_config_data: ModelConfig = Field(..., alias="model_config", description="Model configuration")
model_config_data: ModelConfig = Field(
...,
alias="model_config",
description="Model configuration",
)
current_graph: dict | None = Field(
default=None,
description="Existing draft graph to refine (cmd+k `/refine`); omit for create-from-scratch",
)
class GeneratorResponse(RootModel[Any]):
root: Any
register_enum_models(console_ns, LLMMode)
register_schema_models(
console_ns,
@ -80,6 +93,7 @@ register_schema_models(
WorkflowGeneratePayload,
ModelConfig,
)
register_response_schema_models(console_ns, GeneratorResponse, SimpleDataResponse)
@console_ns.route("/rule-generate")
@ -87,7 +101,11 @@ class RuleGenerateApi(Resource):
@console_ns.doc("generate_rule_config")
@console_ns.doc(description="Generate rule configuration using LLM")
@console_ns.expect(console_ns.models[RuleGeneratePayload.__name__])
@console_ns.response(200, "Rule configuration generated successfully")
@console_ns.response(
200,
"Rule configuration generated successfully",
console_ns.models[GeneratorResponse.__name__],
)
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(402, "Provider quota exceeded")
@setup_required
@ -116,7 +134,7 @@ class RuleCodeGenerateApi(Resource):
@console_ns.doc("generate_rule_code")
@console_ns.doc(description="Generate code rules using LLM")
@console_ns.expect(console_ns.models[RuleCodeGeneratePayload.__name__])
@console_ns.response(200, "Code rules generated successfully")
@console_ns.response(200, "Code rules generated successfully", console_ns.models[GeneratorResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(402, "Provider quota exceeded")
@setup_required
@ -148,7 +166,7 @@ class RuleStructuredOutputGenerateApi(Resource):
@console_ns.doc("generate_structured_output")
@console_ns.doc(description="Generate structured output rules using LLM")
@console_ns.expect(console_ns.models[RuleStructuredOutputPayload.__name__])
@console_ns.response(200, "Structured output generated successfully")
@console_ns.response(200, "Structured output generated successfully", console_ns.models[GeneratorResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(402, "Provider quota exceeded")
@setup_required
@ -180,7 +198,7 @@ class InstructionGenerateApi(Resource):
@console_ns.doc("generate_instruction")
@console_ns.doc(description="Generate instruction for workflow nodes or general use")
@console_ns.expect(console_ns.models[InstructionGeneratePayload.__name__])
@console_ns.response(200, "Instruction generated successfully")
@console_ns.response(200, "Instruction generated successfully", console_ns.models[GeneratorResponse.__name__])
@console_ns.response(400, "Invalid request parameters or flow/workflow not found")
@console_ns.response(402, "Provider quota exceeded")
@setup_required
@ -275,7 +293,7 @@ class InstructionGenerationTemplateApi(Resource):
@console_ns.doc("get_instruction_template")
@console_ns.doc(description="Get instruction generation template")
@console_ns.expect(console_ns.models[InstructionTemplatePayload.__name__])
@console_ns.response(200, "Template retrieved successfully")
@console_ns.response(200, "Template retrieved successfully", console_ns.models[SimpleDataResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@setup_required
@login_required
@ -307,7 +325,7 @@ class WorkflowGenerateApi(Resource):
@console_ns.doc("generate_workflow_graph")
@console_ns.doc(description="Generate a Dify workflow graph from natural language")
@console_ns.expect(console_ns.models[WorkflowGeneratePayload.__name__])
@console_ns.response(200, "Workflow graph generated successfully")
@console_ns.response(200, "Workflow graph generated successfully", console_ns.models[GeneratorResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(402, "Provider quota exceeded")
@setup_required

View File

@ -27,13 +27,19 @@ from models.model import App, AppMCPServer
class MCPServerCreatePayload(BaseModel):
description: str | None = Field(default=None, description="Server description")
parameters: dict[str, Any] = Field(..., description="Server parameters configuration")
parameters: dict[str, Any] = Field(
...,
description="Server parameters configuration",
)
class MCPServerUpdatePayload(BaseModel):
id: str = Field(..., description="Server ID")
description: str | None = Field(default=None, description="Server description")
parameters: dict[str, Any] = Field(..., description="Server parameters configuration")
parameters: dict[str, Any] = Field(
...,
description="Server parameters configuration",
)
status: str | None = Field(default=None, description="Server status")

View File

@ -10,7 +10,7 @@ from sqlalchemy import exists, func, select
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.common.controller_schemas import MessageFeedbackPayload as _MessageFeedbackPayloadBase
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import SimpleResultResponse, TextFileResponse
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.app.error import (
@ -166,7 +166,7 @@ register_schema_models(
MessageDetailResponse,
MessageInfiniteScrollPaginationResponse,
)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(console_ns, SimpleResultResponse, TextFileResponse)
@console_ns.route("/apps/<uuid:app_id>/chat-messages")
@ -373,7 +373,11 @@ class MessageFeedbackExportApi(Resource):
@console_ns.doc(description="Export user feedback data for Google Sheets")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(FeedbackExportQuery))
@console_ns.response(200, "Feedback data exported successfully")
@console_ns.response(
200,
"Feedback data exported successfully",
console_ns.models[TextFileResponse.__name__],
)
@console_ns.response(400, "Invalid parameters")
@console_ns.response(500, "Internal server error")
@get_app_model

View File

@ -5,7 +5,8 @@ from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.schema import register_schema_models
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
@ -29,19 +30,44 @@ from services.app_model_config_service import AppModelConfigService
class ModelConfigRequest(BaseModel):
provider: str | None = Field(default=None, description="Model provider")
model: str | None = Field(default=None, description="Model name")
configs: dict[str, Any] | None = Field(default=None, description="Model configuration parameters")
configs: dict[str, Any] | None = Field(
default=None,
description="Model configuration parameters",
)
opening_statement: str | None = Field(default=None, description="Opening statement")
suggested_questions: list[str] | None = Field(default=None, description="Suggested questions")
more_like_this: dict[str, Any] | None = Field(default=None, description="More like this configuration")
speech_to_text: dict[str, Any] | None = Field(default=None, description="Speech to text configuration")
text_to_speech: dict[str, Any] | None = Field(default=None, description="Text to speech configuration")
retrieval_model: dict[str, Any] | None = Field(default=None, description="Retrieval model configuration")
tools: list[dict[str, Any]] | None = Field(default=None, description="Available tools")
dataset_configs: dict[str, Any] | None = Field(default=None, description="Dataset configurations")
agent_mode: dict[str, Any] | None = Field(default=None, description="Agent mode configuration")
more_like_this: dict[str, Any] | None = Field(
default=None,
description="More like this configuration",
)
speech_to_text: dict[str, Any] | None = Field(
default=None,
description="Speech to text configuration",
)
text_to_speech: dict[str, Any] | None = Field(
default=None,
description="Text to speech configuration",
)
retrieval_model: dict[str, Any] | None = Field(
default=None,
description="Retrieval model configuration",
)
tools: list[dict[str, Any]] | None = Field(
default=None,
description="Available tools",
)
dataset_configs: dict[str, Any] | None = Field(
default=None,
description="Dataset configurations",
)
agent_mode: dict[str, Any] | None = Field(
default=None,
description="Agent mode configuration",
)
register_schema_models(console_ns, ModelConfigRequest)
register_response_schema_models(console_ns, SimpleResultResponse)
@console_ns.route("/apps/<uuid:app_id>/model-config")
@ -50,7 +76,11 @@ class ModelConfigResource(Resource):
@console_ns.doc(description="Update application model configuration")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[ModelConfigRequest.__name__])
@console_ns.response(200, "Model configuration updated successfully")
@console_ns.response(
200,
"Model configuration updated successfully",
console_ns.models[SimpleResultResponse.__name__],
)
@console_ns.response(400, "Invalid configuration")
@console_ns.response(404, "App not found")
@setup_required

View File

@ -1,15 +1,16 @@
from typing import Any
from flask import request
from flask_restx import Resource, fields
from flask_restx import Resource
from pydantic import BaseModel, Field
from werkzeug.exceptions import BadRequest
from controllers.common.schema import query_params_from_model, register_schema_models
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.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required
from fields.base import ResponseModel
from libs.login import login_required
from models import App
from services.ops_service import OpsService
@ -21,10 +22,27 @@ class TraceProviderQuery(BaseModel):
class TraceConfigPayload(BaseModel):
tracing_provider: str = Field(..., description="Tracing provider name")
tracing_config: dict[str, Any] = Field(..., description="Tracing configuration data")
tracing_config: dict[str, Any] = Field(
...,
description="Tracing configuration data",
)
class TraceAppConfigResponse(ResponseModel):
result: str | None = None
error: str | None = None
has_not_configured: bool | None = None
id: str | None = None
app_id: str | None = None
tracing_provider: str | None = None
tracing_config: dict[str, Any] | None = Field(default=None)
is_active: bool | None = None
created_at: str | None = None
updated_at: str | None = None
register_schema_models(console_ns, TraceProviderQuery, TraceConfigPayload)
register_response_schema_models(console_ns, TraceAppConfigResponse)
@console_ns.route("/apps/<uuid:app_id>/trace-config")
@ -38,7 +56,9 @@ class TraceAppConfigApi(Resource):
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(TraceProviderQuery))
@console_ns.response(
200, "Tracing configuration retrieved successfully", fields.Raw(description="Tracing configuration data")
200,
"Tracing configuration retrieved successfully",
console_ns.models[TraceAppConfigResponse.__name__],
)
@console_ns.response(400, "Invalid request parameters")
@setup_required
@ -63,7 +83,9 @@ class TraceAppConfigApi(Resource):
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[TraceConfigPayload.__name__])
@console_ns.response(
201, "Tracing configuration created successfully", fields.Raw(description="Created configuration data")
201,
"Tracing configuration created successfully",
console_ns.models[TraceAppConfigResponse.__name__],
)
@console_ns.response(400, "Invalid request parameters or configuration already exists")
@setup_required
@ -90,7 +112,11 @@ class TraceAppConfigApi(Resource):
@console_ns.doc(description="Update an existing tracing configuration for an application")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[TraceConfigPayload.__name__])
@console_ns.response(200, "Tracing configuration updated successfully", fields.Raw(description="Success response"))
@console_ns.response(
200,
"Tracing configuration updated successfully",
console_ns.models[TraceAppConfigResponse.__name__],
)
@console_ns.response(400, "Invalid request parameters or configuration not found")
@setup_required
@login_required
@ -113,7 +139,7 @@ class TraceAppConfigApi(Resource):
@console_ns.doc("delete_trace_app_config")
@console_ns.doc(description="Delete an existing tracing configuration for an application")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[TraceProviderQuery.__name__])
@console_ns.doc(params=query_params_from_model(TraceProviderQuery))
@console_ns.response(204, "Tracing configuration deleted successfully")
@console_ns.response(400, "Invalid request parameters or configuration not found")
@setup_required

View File

@ -2,15 +2,16 @@ from decimal import Decimal
import sqlalchemy as sa
from flask import abort, jsonify, request
from flask_restx import Resource, fields
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from controllers.common.schema import query_params_from_model, register_schema_models
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.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.datetime_utils import parse_time_range
from libs.helper import convert_datetime_to_date
from libs.login import login_required
@ -31,7 +32,92 @@ class StatisticTimeRangeQuery(BaseModel):
return value
class DailyMessageStatisticItem(ResponseModel):
date: str
message_count: int
class DailyMessageStatisticResponse(ResponseModel):
data: list[DailyMessageStatisticItem]
class DailyConversationStatisticItem(ResponseModel):
date: str
conversation_count: int
class DailyConversationStatisticResponse(ResponseModel):
data: list[DailyConversationStatisticItem]
class DailyTerminalStatisticItem(ResponseModel):
date: str
terminal_count: int
class DailyTerminalStatisticResponse(ResponseModel):
data: list[DailyTerminalStatisticItem]
class DailyTokenCostStatisticItem(ResponseModel):
date: str
token_count: int
total_price: str | float
currency: str
class DailyTokenCostStatisticResponse(ResponseModel):
data: list[DailyTokenCostStatisticItem]
class AverageSessionInteractionStatisticItem(ResponseModel):
date: str
interactions: float
class AverageSessionInteractionStatisticResponse(ResponseModel):
data: list[AverageSessionInteractionStatisticItem]
class UserSatisfactionRateStatisticItem(ResponseModel):
date: str
rate: float
class UserSatisfactionRateStatisticResponse(ResponseModel):
data: list[UserSatisfactionRateStatisticItem]
class AverageResponseTimeStatisticItem(ResponseModel):
date: str
latency: float
class AverageResponseTimeStatisticResponse(ResponseModel):
data: list[AverageResponseTimeStatisticItem]
class TokensPerSecondStatisticItem(ResponseModel):
date: str
tps: float
class TokensPerSecondStatisticResponse(ResponseModel):
data: list[TokensPerSecondStatisticItem]
register_schema_models(console_ns, StatisticTimeRangeQuery)
register_response_schema_models(
console_ns,
DailyMessageStatisticResponse,
DailyConversationStatisticResponse,
DailyTerminalStatisticResponse,
DailyTokenCostStatisticResponse,
AverageSessionInteractionStatisticResponse,
UserSatisfactionRateStatisticResponse,
AverageResponseTimeStatisticResponse,
TokensPerSecondStatisticResponse,
)
@console_ns.route("/apps/<uuid:app_id>/statistics/daily-messages")
@ -43,7 +129,7 @@ class DailyMessageStatistic(Resource):
@console_ns.response(
200,
"Daily message statistics retrieved successfully",
fields.List(fields.Raw(description="Daily message count data")),
console_ns.models[DailyMessageStatisticResponse.__name__],
)
@get_app_model
@setup_required
@ -103,7 +189,7 @@ class DailyConversationStatistic(Resource):
@console_ns.response(
200,
"Daily conversation statistics retrieved successfully",
fields.List(fields.Raw(description="Daily conversation count data")),
console_ns.models[DailyConversationStatisticResponse.__name__],
)
@get_app_model
@setup_required
@ -162,7 +248,7 @@ class DailyTerminalsStatistic(Resource):
@console_ns.response(
200,
"Daily terminal statistics retrieved successfully",
fields.List(fields.Raw(description="Daily terminal count data")),
console_ns.models[DailyTerminalStatisticResponse.__name__],
)
@get_app_model
@setup_required
@ -222,7 +308,7 @@ class DailyTokenCostStatistic(Resource):
@console_ns.response(
200,
"Daily token cost statistics retrieved successfully",
fields.List(fields.Raw(description="Daily token cost data")),
console_ns.models[DailyTokenCostStatisticResponse.__name__],
)
@get_app_model
@setup_required
@ -285,7 +371,7 @@ class AverageSessionInteractionStatistic(Resource):
@console_ns.response(
200,
"Average session interaction statistics retrieved successfully",
fields.List(fields.Raw(description="Average session interaction data")),
console_ns.models[AverageSessionInteractionStatisticResponse.__name__],
)
@setup_required
@login_required
@ -364,7 +450,7 @@ class UserSatisfactionRateStatistic(Resource):
@console_ns.response(
200,
"User satisfaction rate statistics retrieved successfully",
fields.List(fields.Raw(description="User satisfaction rate data")),
console_ns.models[UserSatisfactionRateStatisticResponse.__name__],
)
@get_app_model
@setup_required
@ -433,7 +519,7 @@ class AverageResponseTimeStatistic(Resource):
@console_ns.response(
200,
"Average response time statistics retrieved successfully",
fields.List(fields.Raw(description="Average response time data")),
console_ns.models[AverageResponseTimeStatisticResponse.__name__],
)
@setup_required
@login_required
@ -493,7 +579,7 @@ class TokensPerSecondStatistic(Resource):
@console_ns.response(
200,
"Tokens per second statistics retrieved successfully",
fields.List(fields.Raw(description="Tokens per second data")),
console_ns.models[TokensPerSecondStatisticResponse.__name__],
)
@get_app_model
@setup_required

View File

@ -6,14 +6,14 @@ from typing import Any, NotRequired, TypedDict
from flask import abort, request
from flask_restx import Resource, fields
from pydantic import AliasChoices, BaseModel, Field, ValidationError, field_validator
from pydantic import AliasChoices, BaseModel, Field, RootModel, ValidationError, field_validator
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
import services
from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload
from controllers.common.errors import InvalidArgumentError
from controllers.common.fields import NewAppResponse, SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, NewAppResponse, SimpleResultResponse
from controllers.common.schema import (
query_params_from_model,
register_response_schema_model,
@ -99,16 +99,20 @@ class SyncDraftWorkflowPayload(BaseModel):
graph: dict[str, Any]
features: dict[str, Any]
hash: str | None = None
environment_variables: list[dict[str, Any]] = Field(default_factory=list)
conversation_variables: list[dict[str, Any]] = Field(default_factory=list)
environment_variables: list[dict[str, Any]] = Field(
default_factory=list,
)
conversation_variables: list[dict[str, Any]] = Field(
default_factory=list,
)
class BaseWorkflowRunPayload(BaseModel):
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
class AdvancedChatWorkflowRunPayload(BaseWorkflowRunPayload):
inputs: dict[str, Any] | None = None
inputs: dict[str, Any] | None = Field(default=None)
query: str = ""
conversation_id: str | None = None
parent_message_id: str | None = None
@ -122,11 +126,11 @@ class AdvancedChatWorkflowRunPayload(BaseWorkflowRunPayload):
class IterationNodeRunPayload(BaseModel):
inputs: dict[str, Any] | None = None
inputs: dict[str, Any] | None = Field(default=None)
class LoopNodeRunPayload(BaseModel):
inputs: dict[str, Any] | None = None
inputs: dict[str, Any] | None = Field(default=None)
class DraftWorkflowRunPayload(BaseWorkflowRunPayload):
@ -151,7 +155,10 @@ class ConvertToWorkflowPayload(BaseModel):
class WorkflowFeaturesPayload(BaseModel):
features: dict[str, Any] = Field(..., description="Workflow feature configuration")
features: dict[str, Any] = Field(
...,
description="Workflow feature configuration",
)
class WorkflowOnlineUsersPayload(BaseModel):
@ -167,7 +174,7 @@ class WorkflowConversationVariableResponse(ResponseModel):
id: str
name: str
value_type: str
value: Any = Field(json_schema_extra={"type": "object"})
value: Any
description: str
@field_validator("value_type", mode="before")
@ -186,7 +193,7 @@ class PipelineVariableResponse(ResponseModel):
max_length: int | None = None
required: bool
unit: str | None = None
default_value: Any = Field(default=None, json_schema_extra={"type": "object"})
default_value: Any = Field(default=None)
options: list[str] | None = None
placeholder: str | None = None
tooltips: str | None = None
@ -203,14 +210,18 @@ class WorkflowEnvironmentVariableResponse(ResponseModel):
value_type: str
id: str
name: str
value: Any = Field(json_schema_extra={"type": "object"})
value: Any
description: str
class WorkflowResponse(ResponseModel):
id: str
graph: dict[str, Any] = Field(validation_alias=AliasChoices("graph_dict", "graph"))
features: dict[str, Any] = Field(validation_alias=AliasChoices("features_dict", "features"))
graph: dict[str, Any] = Field(
validation_alias=AliasChoices("graph_dict", "graph"),
)
features: dict[str, Any] = Field(
validation_alias=AliasChoices("features_dict", "features"),
)
hash: str = Field(validation_alias=AliasChoices("unique_hash", "hash"))
version: str
marked_name: str
@ -267,6 +278,46 @@ class WorkflowOnlineUsersResponse(ResponseModel):
data: list[WorkflowOnlineUsersByApp]
class WorkflowPublishResponse(ResponseModel):
result: str
created_at: int
class WorkflowRestoreResponse(ResponseModel):
result: str
hash: str
updated_at: int
class DefaultBlockConfigsResponse(RootModel[list[dict[str, Any]]]):
root: list[dict[str, Any]]
class DefaultBlockConfigResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class HumanInputFormPreviewResponse(ResponseModel):
form_id: str
node_id: str
node_title: str
form_content: str
inputs: list[dict[str, Any]] = Field(default_factory=list)
actions: list[dict[str, Any]] = Field(default_factory=list)
display_in_ui: bool | None = None
form_token: str | None = None
resolved_default_values: dict[str, Any] = Field(default_factory=dict)
expiration_time: int | None = None
class HumanInputFormSubmitResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class EmptyObjectResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class DraftWorkflowTriggerRunPayload(BaseModel):
node_id: str
@ -304,6 +355,14 @@ register_response_schema_models(
WorkflowOnlineUser,
WorkflowOnlineUsersByApp,
WorkflowOnlineUsersResponse,
WorkflowPublishResponse,
WorkflowRestoreResponse,
DefaultBlockConfigsResponse,
DefaultBlockConfigResponse,
HumanInputFormPreviewResponse,
HumanInputFormSubmitResponse,
EmptyObjectResponse,
GeneratedAppResponse,
NewAppResponse,
SimpleResultResponse,
)
@ -475,7 +534,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
@console_ns.doc(description="Run draft workflow for advanced chat application")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[AdvancedChatWorkflowRunPayload.__name__])
@console_ns.response(200, "Workflow run started successfully")
@console_ns.response(200, "Workflow run started successfully", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(403, "Permission denied")
@setup_required
@ -520,7 +579,11 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
@console_ns.doc(description="Run draft workflow iteration node for advanced chat")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[IterationNodeRunPayload.__name__])
@console_ns.response(200, "Iteration node run started successfully")
@console_ns.response(
200,
"Iteration node run started successfully",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(403, "Permission denied")
@console_ns.response(404, "Node not found")
@setup_required
@ -558,7 +621,11 @@ class WorkflowDraftRunIterationNodeApi(Resource):
@console_ns.doc(description="Run draft workflow iteration node")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[IterationNodeRunPayload.__name__])
@console_ns.response(200, "Workflow iteration node run started successfully")
@console_ns.response(
200,
"Workflow iteration node run started successfully",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(403, "Permission denied")
@console_ns.response(404, "Node not found")
@setup_required
@ -596,7 +663,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
@console_ns.doc(description="Run draft workflow loop node for advanced chat")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[LoopNodeRunPayload.__name__])
@console_ns.response(200, "Loop node run started successfully")
@console_ns.response(200, "Loop node run started successfully", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(403, "Permission denied")
@console_ns.response(404, "Node not found")
@setup_required
@ -634,7 +701,11 @@ class WorkflowDraftRunLoopNodeApi(Resource):
@console_ns.doc(description="Run draft workflow loop node")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[LoopNodeRunPayload.__name__])
@console_ns.response(200, "Workflow loop node run started successfully")
@console_ns.response(
200,
"Workflow loop node run started successfully",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(403, "Permission denied")
@console_ns.response(404, "Node not found")
@setup_required
@ -674,7 +745,10 @@ class HumanInputFormPreviewPayload(BaseModel):
class HumanInputFormSubmitPayload(BaseModel):
form_inputs: dict[str, Any] = Field(..., description="Values the user provides for the form's own fields")
form_inputs: dict[str, Any] = Field(
...,
description="Values the user provides for the form's own fields",
)
inputs: dict[str, Any] = Field(
...,
description="Values used to fill missing upstream variables referenced in form_content",
@ -704,6 +778,7 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource):
@console_ns.doc(description="Get human input form preview for advanced chat workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputFormPreviewPayload.__name__])
@console_ns.response(200, "Human input form preview", console_ns.models[HumanInputFormPreviewResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -733,6 +808,11 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource):
@console_ns.doc(description="Submit human input form preview for advanced chat workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__])
@console_ns.response(
200,
"Human input form submission result",
console_ns.models[HumanInputFormSubmitResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -762,6 +842,7 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource):
@console_ns.doc(description="Get human input form preview for workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputFormPreviewPayload.__name__])
@console_ns.response(200, "Human input form preview", console_ns.models[HumanInputFormPreviewResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -791,6 +872,11 @@ class WorkflowDraftHumanInputFormRunApi(Resource):
@console_ns.doc(description="Submit human input form preview for workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__])
@console_ns.response(
200,
"Human input form submission result",
console_ns.models[HumanInputFormSubmitResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -820,6 +906,7 @@ class WorkflowDraftHumanInputDeliveryTestApi(Resource):
@console_ns.doc(description="Test human input delivery for workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputDeliveryTestPayload.__name__])
@console_ns.response(200, "Human input delivery test result", console_ns.models[EmptyObjectResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -848,7 +935,11 @@ class DraftWorkflowRunApi(Resource):
@console_ns.doc(description="Run draft workflow")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[DraftWorkflowRunPayload.__name__])
@console_ns.response(200, "Draft workflow run started successfully")
@console_ns.response(
200,
"Draft workflow run started successfully",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(403, "Permission denied")
@setup_required
@login_required
@ -990,6 +1081,7 @@ class PublishedWorkflowApi(Resource):
return dump_response(WorkflowResponse, workflow)
@console_ns.expect(console_ns.models[PublishWorkflowPayload.__name__])
@console_ns.response(200, "Workflow published successfully", console_ns.models[WorkflowPublishResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1033,7 +1125,11 @@ class DefaultBlockConfigsApi(Resource):
@console_ns.doc("get_default_block_configs")
@console_ns.doc(description="Get default block configurations for workflow")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Default block configurations retrieved successfully")
@console_ns.response(
200,
"Default block configurations retrieved successfully",
console_ns.models[DefaultBlockConfigsResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -1053,7 +1149,11 @@ class DefaultBlockConfigApi(Resource):
@console_ns.doc("get_default_block_config")
@console_ns.doc(description="Get default block configuration by type")
@console_ns.doc(params={"app_id": "Application ID", "block_type": "Block type"})
@console_ns.response(200, "Default block configuration retrieved successfully")
@console_ns.response(
200,
"Default block configuration retrieved successfully",
console_ns.models[DefaultBlockConfigResponse.__name__],
)
@console_ns.response(404, "Block type not found")
@console_ns.doc(params=query_params_from_model(DefaultBlockConfigQuery))
@setup_required
@ -1205,7 +1305,7 @@ class DraftWorkflowRestoreApi(Resource):
@console_ns.doc("restore_workflow_to_draft")
@console_ns.doc(description="Restore a published workflow version into the draft workflow")
@console_ns.doc(params={"app_id": "Application ID", "workflow_id": "Published workflow ID"})
@console_ns.response(200, "Workflow restored successfully")
@console_ns.response(200, "Workflow restored successfully", console_ns.models[WorkflowRestoreResponse.__name__])
@console_ns.response(400, "Source workflow must be published")
@console_ns.response(404, "Workflow not found")
@setup_required
@ -1290,6 +1390,7 @@ class WorkflowByIdApi(Resource):
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@edit_permission_required
@console_ns.response(204, "Workflow deleted successfully")
def delete(self, app_model: App, workflow_id: str):
"""
Delete workflow
@ -1361,7 +1462,11 @@ class DraftWorkflowTriggerRunApi(Resource):
},
)
)
@console_ns.response(200, "Trigger event received and workflow executed successfully")
@console_ns.response(
200,
"Trigger event received and workflow executed successfully",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(403, "Permission denied")
@console_ns.response(500, "Internal server error")
@setup_required
@ -1425,7 +1530,11 @@ class DraftWorkflowTriggerNodeApi(Resource):
@console_ns.doc("poll_draft_workflow_trigger_node")
@console_ns.doc(description="Poll for trigger events and execute single node when event arrives")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.response(200, "Trigger event received and node executed successfully")
@console_ns.response(
200,
"Trigger event received and node executed successfully",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(403, "Permission denied")
@console_ns.response(500, "Internal server error")
@setup_required
@ -1505,7 +1614,7 @@ class DraftWorkflowTriggerRunAllApi(Resource):
@console_ns.doc(description="Full workflow debug when the start node is a trigger")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[DraftWorkflowTriggerRunAllPayload.__name__])
@console_ns.response(200, "Workflow executed successfully")
@console_ns.response(200, "Workflow executed successfully", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(403, "Permission denied")
@console_ns.response(500, "Internal server error")
@setup_required

View File

@ -1,7 +1,7 @@
import logging
from collections.abc import Callable
from functools import wraps
from typing import Any, Concatenate, TypedDict
from typing import Any, Concatenate, TypedDict, override
from uuid import UUID
from flask import Response, request
@ -10,7 +10,8 @@ from pydantic import BaseModel, Field
from sqlalchemy.orm import sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.common.schema import query_params_from_model, register_schema_models
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.app.error import (
DraftWorkflowNotExist,
@ -28,6 +29,7 @@ from extensions.ext_database import db
from factories import variable_factory
from factories.file_factory import build_from_mapping, build_from_mappings
from factories.variable_factory import build_segment_with_type
from fields.base import ResponseModel
from graphon.file import helpers as file_helpers
from graphon.variables.segment_group import SegmentGroup
from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment
@ -42,6 +44,28 @@ logger = logging.getLogger(__name__)
_file_access_controller = DatabaseFileAccessController()
class OpaqueRawField(fields.Raw):
@override
def schema(self) -> dict[str, object]:
return {"type": "object"}
class JsonValueRawField(fields.Raw):
@override
def schema(self) -> dict[str, object]:
return {
"anyOf": [
{"type": "string"},
{"type": "integer"},
{"type": "number"},
{"type": "boolean"},
{"type": "object", "additionalProperties": True},
{"type": "array", "items": {}},
{"type": "null"},
]
}
class WorkflowDraftVariableListQuery(BaseModel):
page: int = Field(default=1, ge=1, le=100_000, description="Page number")
limit: int = Field(default=20, ge=1, le=100, description="Items per page")
@ -54,12 +78,33 @@ class WorkflowDraftVariableUpdatePayload(BaseModel):
class ConversationVariableUpdatePayload(BaseModel):
conversation_variables: list[dict[str, Any]] = Field(
..., description="Conversation variables for the draft workflow"
...,
description="Conversation variables for the draft workflow",
)
class EnvironmentVariableUpdatePayload(BaseModel):
environment_variables: list[dict[str, Any]] = Field(..., description="Environment variables for the draft workflow")
environment_variables: list[dict[str, Any]] = Field(
...,
description="Environment variables for the draft workflow",
)
class EnvironmentVariableItemResponse(ResponseModel):
id: str
type: str
name: str
description: str | None = None
selector: list[str]
value_type: str
value: Any
edited: bool
visible: bool
editable: bool
class EnvironmentVariableListResponse(ResponseModel):
items: list[EnvironmentVariableItemResponse]
register_schema_models(
@ -69,6 +114,7 @@ register_schema_models(
ConversationVariableUpdatePayload,
EnvironmentVariableUpdatePayload,
)
register_response_schema_models(console_ns, SimpleResultResponse, EnvironmentVariableListResponse)
def _convert_values_to_json_serializable_object(value: Segment):
@ -155,8 +201,8 @@ _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS = {
_WORKFLOW_DRAFT_VARIABLE_FIELDS = {
**_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS,
"value": fields.Raw(attribute=_serialize_var_value),
"full_content": fields.Raw(attribute=_serialize_full_content),
"value": JsonValueRawField(attribute=_serialize_var_value),
"full_content": OpaqueRawField(attribute=_serialize_full_content),
}
_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS = {
@ -181,7 +227,7 @@ def _get_items(var_list: WorkflowDraftVariableList) -> list[WorkflowDraftVariabl
_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS = {
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS), attribute=_get_items),
"total": fields.Raw(),
"total": fields.Integer,
}
_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS = {
@ -544,7 +590,11 @@ class ConversationVariableCollectionApi(Resource):
@console_ns.doc("update_conversation_variables")
@console_ns.doc(description="Update conversation variables for workflow draft")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Conversation variables updated successfully")
@console_ns.response(
200,
"Conversation variables updated successfully",
console_ns.models[SimpleResultResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -587,7 +637,11 @@ class EnvironmentVariableCollectionApi(Resource):
@console_ns.doc("get_environment_variables")
@console_ns.doc(description="Get environment variables for workflow")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Environment variables retrieved successfully")
@console_ns.response(
200,
"Environment variables retrieved successfully",
console_ns.models[EnvironmentVariableListResponse.__name__],
)
@console_ns.response(404, "Draft workflow not found")
@_api_prerequisite
def get(self, _current_user: Account, app_model: App):
@ -625,7 +679,11 @@ class EnvironmentVariableCollectionApi(Resource):
@console_ns.doc("update_environment_variables")
@console_ns.doc(description="Update environment variables for workflow draft")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Environment variables updated successfully")
@console_ns.response(
200,
"Environment variables updated successfully",
console_ns.models[SimpleResultResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required

View File

@ -30,6 +30,7 @@ from uuid import UUID
from flask import Response
from flask_restx import Resource
from controllers.common.fields import EventStreamResponse
from controllers.common.schema import register_response_schema_models
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model
@ -62,6 +63,7 @@ _STREAM_HARD_TIMEOUT_TICKS = 1800 # 30 min
register_response_schema_models(
console_ns,
EventStreamResponse,
CheckResultView,
NodeOutputView,
NodeOutputsView,
@ -327,7 +329,11 @@ class WorkflowDraftRunNodeOutputEventsApi(Resource):
@console_ns.doc("stream_workflow_draft_run_node_output_events")
@console_ns.doc(description="Server-Sent Events stream of inspector deltas for a draft workflow run.")
@console_ns.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"})
@console_ns.response(200, "Workflow run node output event stream")
@console_ns.response(
200,
"Workflow run node output event stream",
console_ns.models[EventStreamResponse.__name__],
)
@console_ns.response(404, "Workflow run not found")
@setup_required
@login_required
@ -424,7 +430,11 @@ class WorkflowPublishedRunNodeOutputEventsApi(Resource):
@console_ns.doc("stream_workflow_published_run_node_output_events")
@console_ns.doc(description="Server-Sent Events stream of inspector deltas for a published workflow run.")
@console_ns.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"})
@console_ns.response(200, "Workflow run node output event stream")
@console_ns.response(
200,
"Workflow run node output event stream",
console_ns.models[EventStreamResponse.__name__],
)
@console_ns.response(404, "Workflow run not found")
@setup_required
@login_required

View File

@ -3,11 +3,12 @@ from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
from controllers.common.schema import query_params_from_model, register_schema_models
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.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.datetime_utils import parse_time_range
from libs.login import login_required
from models.account import Account
@ -28,7 +29,50 @@ class WorkflowStatisticQuery(BaseModel):
return value
class WorkflowDailyRunsStatisticItem(ResponseModel):
date: str
runs: int
class WorkflowDailyRunsStatisticResponse(ResponseModel):
data: list[WorkflowDailyRunsStatisticItem]
class WorkflowDailyTerminalsStatisticItem(ResponseModel):
date: str
terminal_count: int
class WorkflowDailyTerminalsStatisticResponse(ResponseModel):
data: list[WorkflowDailyTerminalsStatisticItem]
class WorkflowDailyTokenCostStatisticItem(ResponseModel):
date: str
token_count: int
class WorkflowDailyTokenCostStatisticResponse(ResponseModel):
data: list[WorkflowDailyTokenCostStatisticItem]
class WorkflowAverageAppInteractionStatisticItem(ResponseModel):
date: str
interactions: float
class WorkflowAverageAppInteractionStatisticResponse(ResponseModel):
data: list[WorkflowAverageAppInteractionStatisticItem]
register_schema_models(console_ns, WorkflowStatisticQuery)
register_response_schema_models(
console_ns,
WorkflowDailyRunsStatisticResponse,
WorkflowDailyTerminalsStatisticResponse,
WorkflowDailyTokenCostStatisticResponse,
WorkflowAverageAppInteractionStatisticResponse,
)
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
@ -42,7 +86,11 @@ class WorkflowDailyRunsStatistic(Resource):
@console_ns.doc(description="Get workflow daily runs statistics")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery))
@console_ns.response(200, "Daily runs statistics retrieved successfully")
@console_ns.response(
200,
"Daily runs statistics retrieved successfully",
console_ns.models[WorkflowDailyRunsStatisticResponse.__name__],
)
@get_app_model
@setup_required
@login_required
@ -81,7 +129,11 @@ class WorkflowDailyTerminalsStatistic(Resource):
@console_ns.doc(description="Get workflow daily terminals statistics")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery))
@console_ns.response(200, "Daily terminals statistics retrieved successfully")
@console_ns.response(
200,
"Daily terminals statistics retrieved successfully",
console_ns.models[WorkflowDailyTerminalsStatisticResponse.__name__],
)
@get_app_model
@setup_required
@login_required
@ -120,7 +172,11 @@ class WorkflowDailyTokenCostStatistic(Resource):
@console_ns.doc(description="Get workflow daily token cost statistics")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery))
@console_ns.response(200, "Daily token cost statistics retrieved successfully")
@console_ns.response(
200,
"Daily token cost statistics retrieved successfully",
console_ns.models[WorkflowDailyTokenCostStatisticResponse.__name__],
)
@get_app_model
@setup_required
@login_required
@ -159,7 +215,11 @@ class WorkflowAverageAppInteractionStatistic(Resource):
@console_ns.doc(description="Get workflow average app interaction statistics")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery))
@console_ns.response(200, "Average app interaction statistics retrieved successfully")
@console_ns.response(
200,
"Average app interaction statistics retrieved successfully",
console_ns.models[WorkflowAverageAppInteractionStatisticResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required

View File

@ -3,6 +3,7 @@ from uuid import UUID
from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from fields.base import ResponseModel
from libs.login import login_required
@ -33,7 +34,12 @@ class ApiKeyAuthDataSourceListResponse(ResponseModel):
register_schema_models(console_ns, ApiKeyAuthBindingPayload)
register_response_schema_models(console_ns, ApiKeyAuthDataSourceItem, ApiKeyAuthDataSourceListResponse)
register_response_schema_models(
console_ns,
SimpleResultResponse,
ApiKeyAuthDataSourceItem,
ApiKeyAuthDataSourceListResponse,
)
@console_ns.route("/api-key-auth/data-source")
@ -64,6 +70,7 @@ class ApiKeyAuthDataSource(Resource):
@console_ns.route("/api-key-auth/data-source/binding")
class ApiKeyAuthDataSourceBinding(Resource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -7,7 +7,8 @@ from flask_restx import Resource
from pydantic import BaseModel, Field
from configs import dify_config
from controllers.common.schema import register_schema_models
from controllers.common.fields import RedirectResponse
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_models
from libs.login import login_required
from libs.oauth_data_source import NotionOAuth
@ -29,12 +30,24 @@ class OAuthDataSourceSyncResponse(BaseModel):
result: str = Field(description="Operation result")
class OAuthDataSourceCallbackQuery(BaseModel):
code: str | None = Field(default=None, description="Authorization code from OAuth provider")
error: str | None = Field(default=None, description="Error message from OAuth provider")
class OAuthDataSourceBindingQuery(BaseModel):
code: str = Field(description="Authorization code from OAuth provider")
register_schema_models(
console_ns,
OAuthDataSourceResponse,
OAuthDataSourceBindingResponse,
OAuthDataSourceSyncResponse,
OAuthDataSourceCallbackQuery,
OAuthDataSourceBindingQuery,
)
register_response_schema_model(console_ns, RedirectResponse)
def get_oauth_providers():
@ -84,14 +97,9 @@ class OAuthDataSource(Resource):
class OAuthDataSourceCallback(Resource):
@console_ns.doc("oauth_data_source_callback")
@console_ns.doc(description="Handle OAuth callback from data source provider")
@console_ns.doc(
params={
"provider": "Data source provider name (notion)",
"code": "Authorization code from OAuth provider",
"error": "Error message from OAuth provider",
}
)
@console_ns.response(302, "Redirect to console with result")
@console_ns.doc(params={"provider": "Data source provider name (notion)"})
@console_ns.doc(params=query_params_from_model(OAuthDataSourceCallbackQuery))
@console_ns.response(302, "Redirect to console with result", console_ns.models[RedirectResponse.__name__])
@console_ns.response(400, "Invalid provider")
def get(self, provider: str):
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
@ -115,9 +123,8 @@ class OAuthDataSourceCallback(Resource):
class OAuthDataSourceBinding(Resource):
@console_ns.doc("oauth_data_source_binding")
@console_ns.doc(description="Bind OAuth data source with authorization code")
@console_ns.doc(
params={"provider": "Data source provider name (notion)", "code": "Authorization code from OAuth provider"}
)
@console_ns.doc(params={"provider": "Data source provider name (notion)"})
@console_ns.doc(params=query_params_from_model(OAuthDataSourceBindingQuery))
@console_ns.response(
200,
"Data source binding success",

View File

@ -15,6 +15,7 @@ from controllers.console.auth.error import (
InvalidTokenError,
PasswordMismatchError,
)
from fields.base import ResponseModel
from libs.helper import EmailStr, extract_remote_ip
from libs.helper import timezone as validate_timezone_string
from libs.password import valid_password
@ -58,8 +59,24 @@ class EmailRegisterResetPayload(BaseModel):
return validate_timezone_string(value)
class EmailRegisterTokenPairResponse(ResponseModel):
access_token: str
refresh_token: str
csrf_token: str
class EmailRegisterResetResponse(ResponseModel):
result: str
data: EmailRegisterTokenPairResponse
register_schema_models(console_ns, EmailRegisterSendPayload, EmailRegisterValidityPayload, EmailRegisterResetPayload)
register_response_schema_models(console_ns, SimpleResultDataResponse, VerificationTokenResponse)
register_response_schema_models(
console_ns,
SimpleResultDataResponse,
VerificationTokenResponse,
EmailRegisterResetResponse,
)
@console_ns.route("/email-register/send-email")
@ -67,6 +84,7 @@ class EmailRegisterSendEmailApi(Resource):
@setup_required
@email_password_login_enabled
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterSendPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
def post(self):
args = EmailRegisterSendPayload.model_validate(console_ns.payload)
@ -92,6 +110,7 @@ class EmailRegisterCheckApi(Resource):
@setup_required
@email_password_login_enabled
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterValidityPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
def post(self):
args = EmailRegisterValidityPayload.model_validate(console_ns.payload)
@ -133,6 +152,8 @@ class EmailRegisterResetApi(Resource):
@setup_required
@email_password_login_enabled
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterResetPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EmailRegisterResetResponse.__name__])
def post(self):
args = EmailRegisterResetPayload.model_validate(console_ns.payload)

View File

@ -4,10 +4,13 @@ import urllib.parse
import httpx
from flask import current_app, redirect, request
from flask_restx import Resource
from pydantic import BaseModel, Field
from werkzeug.exceptions import Unauthorized
from configs import dify_config
from constants.languages import languages
from controllers.common.fields import RedirectResponse
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_models
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
@ -31,6 +34,21 @@ from .. import console_ns
logger = logging.getLogger(__name__)
class OAuthLoginQuery(BaseModel):
invite_token: str | None = Field(default=None, description="Optional invitation token")
timezone: str | None = Field(default=None, description="Preferred timezone")
language: str | None = Field(default=None, description="Preferred interface language")
class OAuthCallbackQuery(BaseModel):
code: str = Field(description="Authorization code from OAuth provider")
state: str | None = Field(default=None, description="OAuth state parameter")
register_schema_models(console_ns, OAuthLoginQuery, OAuthCallbackQuery)
register_response_schema_model(console_ns, RedirectResponse)
def get_oauth_providers():
with current_app.app_context():
if not dify_config.GITHUB_CLIENT_ID or not dify_config.GITHUB_CLIENT_SECRET:
@ -83,10 +101,9 @@ def _preferred_interface_language(language: str | None = None) -> str:
class OAuthLogin(Resource):
@console_ns.doc("oauth_login")
@console_ns.doc(description="Initiate OAuth login process")
@console_ns.doc(
params={"provider": "OAuth provider name (github/google)", "invite_token": "Optional invitation token"}
)
@console_ns.response(302, "Redirect to OAuth authorization URL")
@console_ns.doc(params={"provider": "OAuth provider name (github/google)"})
@console_ns.doc(params=query_params_from_model(OAuthLoginQuery))
@console_ns.response(302, "Redirect to OAuth authorization URL", console_ns.models[RedirectResponse.__name__])
@console_ns.response(400, "Invalid provider")
def get(self, provider: str):
invite_token = request.args.get("invite_token") or None
@ -110,14 +127,9 @@ class OAuthLogin(Resource):
class OAuthCallback(Resource):
@console_ns.doc("oauth_callback")
@console_ns.doc(description="Handle OAuth callback and complete login process")
@console_ns.doc(
params={
"provider": "OAuth provider name (github/google)",
"code": "Authorization code from OAuth provider",
"state": "Optional state parameter (used for invite token)",
}
)
@console_ns.response(302, "Redirect to console with access token")
@console_ns.doc(params={"provider": "OAuth provider name (github/google)"})
@console_ns.doc(params=query_params_from_model(OAuthCallbackQuery))
@console_ns.response(302, "Redirect to console with access token", console_ns.models[RedirectResponse.__name__])
@console_ns.response(400, "OAuth process failed")
def get(self, provider: str):
OAUTH_PROVIDERS = get_oauth_providers()

View File

@ -1,6 +1,6 @@
from collections.abc import Callable
from functools import wraps
from typing import Concatenate
from typing import Any, Concatenate
from flask import jsonify, request
from flask.typing import ResponseReturnValue
@ -8,6 +8,7 @@ from flask_restx import Resource
from pydantic import BaseModel
from werkzeug.exceptions import BadRequest, NotFound
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.login import login_required
@ -36,6 +37,41 @@ class OAuthTokenRequest(BaseModel):
refresh_token: str | None = None
class OAuthProviderAppResponse(BaseModel):
app_icon: str
app_label: dict[str, Any]
scope: str
class OAuthProviderAuthorizeResponse(BaseModel):
code: str
class OAuthProviderTokenResponse(BaseModel):
access_token: str
token_type: str
expires_in: int
refresh_token: str
class OAuthProviderAccountResponse(BaseModel):
name: str
email: str
avatar: str | None = None
interface_language: str
timezone: str
register_schema_models(console_ns, OAuthClientPayload, OAuthProviderRequest, OAuthTokenRequest)
register_response_schema_models(
console_ns,
OAuthProviderAccountResponse,
OAuthProviderAppResponse,
OAuthProviderAuthorizeResponse,
OAuthProviderTokenResponse,
)
def oauth_server_client_id_required[T, **P, R](
view: Callable[Concatenate[T, OAuthProviderApp, P], R],
) -> Callable[Concatenate[T, P], R]:
@ -110,6 +146,8 @@ def oauth_server_access_token_required[T, **P, R](
@console_ns.route("/oauth/provider")
class OAuthServerAppApi(Resource):
@setup_required
@console_ns.expect(console_ns.models[OAuthProviderRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderAppResponse.__name__])
@oauth_server_client_id_required
def post(self, oauth_provider_app: OAuthProviderApp):
payload = OAuthProviderRequest.model_validate(request.get_json())
@ -134,6 +172,8 @@ class OAuthServerUserAuthorizeApi(Resource):
@login_required
@account_initialization_required
@with_current_user
@console_ns.expect(console_ns.models[OAuthClientPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderAuthorizeResponse.__name__])
@oauth_server_client_id_required
def post(self, oauth_provider_app: OAuthProviderApp, current_user: Account):
user_account_id = current_user.id
@ -148,6 +188,8 @@ class OAuthServerUserAuthorizeApi(Resource):
@console_ns.route("/oauth/provider/token")
class OAuthServerUserTokenApi(Resource):
@setup_required
@console_ns.expect(console_ns.models[OAuthTokenRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderTokenResponse.__name__])
@oauth_server_client_id_required
def post(self, oauth_provider_app: OAuthProviderApp):
payload = OAuthTokenRequest.model_validate(request.get_json())
@ -198,6 +240,8 @@ class OAuthServerUserTokenApi(Resource):
@console_ns.route("/oauth/provider/account")
class OAuthServerUserAccountApi(Resource):
@setup_required
@console_ns.expect(console_ns.models[OAuthClientPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderAccountResponse.__name__])
@oauth_server_client_id_required
@oauth_server_access_token_required
def post(self, oauth_provider_app: OAuthProviderApp, account: Account):

View File

@ -1,12 +1,12 @@
import base64
from typing import Literal
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel
from werkzeug.exceptions import BadRequest
from controllers.common.schema import register_schema_models
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 (
account_initialization_required,
@ -30,11 +30,18 @@ class PartnerTenantsPayload(BaseModel):
click_id: str = Field(..., description="Click Id from partner referral link")
class BillingResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(console_ns, SubscriptionQuery, PartnerTenantsPayload)
register_response_schema_models(console_ns, BillingResponse)
@console_ns.route("/billing/subscription")
class Subscription(Resource):
@console_ns.doc(params=query_params_from_model(SubscriptionQuery))
@console_ns.response(200, "Success", console_ns.models[BillingResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -49,6 +56,7 @@ class Subscription(Resource):
@console_ns.route("/billing/invoices")
class Invoices(Resource):
@console_ns.response(200, "Success", console_ns.models[BillingResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -66,7 +74,7 @@ class PartnerTenants(Resource):
@console_ns.doc(description="Sync partner tenants bindings")
@console_ns.doc(params={"partner_key": "Partner key"})
@console_ns.expect(console_ns.models[PartnerTenantsPayload.__name__])
@console_ns.response(200, "Tenants synced to partner successfully")
@console_ns.response(200, "Tenants synced to partner successfully", console_ns.models[BillingResponse.__name__])
@console_ns.response(400, "Invalid partner information")
@setup_required
@login_required

View File

@ -1,8 +1,10 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel
from controllers.common.schema import query_params_from_model
from controllers.common.schema import query_params_from_model, register_response_schema_models
from libs.helper import extract_remote_ip
from libs.login import login_required
from models import Account
@ -23,10 +25,15 @@ class ComplianceDownloadQuery(BaseModel):
doc_name: str = Field(..., description="Compliance document name")
class ComplianceDownloadResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
console_ns.schema_model(
ComplianceDownloadQuery.__name__,
ComplianceDownloadQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
)
register_response_schema_models(console_ns, ComplianceDownloadResponse)
@console_ns.route("/compliance/download")
@ -34,6 +41,7 @@ class ComplianceApi(Resource):
@console_ns.doc(params=query_params_from_model(ComplianceDownloadQuery))
@console_ns.doc("download_compliance_document")
@console_ns.doc(description="Get compliance document download link")
@console_ns.response(200, "Success", console_ns.models[ComplianceDownloadResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -95,13 +95,13 @@ class DatasetUpdatePayload(BaseModel):
indexing_technique: str | None = None
embedding_model: str | None = None
embedding_model_provider: str | None = None
retrieval_model: dict[str, Any] | None = None
summary_index_setting: dict[str, Any] | None = None
retrieval_model: dict[str, Any] | None = Field(default=None)
summary_index_setting: dict[str, Any] | None = Field(default=None)
partial_member_list: list[dict[str, str]] | None = None
external_retrieval_model: dict[str, Any] | None = None
external_retrieval_model: dict[str, Any] | None = Field(default=None)
external_knowledge_id: str | None = None
external_knowledge_api_id: str | None = None
icon_info: dict[str, Any] | None = None
icon_info: dict[str, Any] | None = Field(default=None)
is_multimodal: bool | None = False
@field_validator("indexing_technique")

View File

@ -10,13 +10,13 @@ from uuid import UUID
import sqlalchemy as sa
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, RootModel, field_validator
from sqlalchemy import asc, desc, func, select
from werkzeug.exceptions import Forbidden, NotFound
import services
from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload
from controllers.common.fields import SimpleResultMessageResponse, SimpleResultResponse, UrlResponse
from controllers.common.fields import BinaryFileResponse, SimpleResultMessageResponse, SimpleResultResponse, UrlResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from core.errors.error import (
@ -145,6 +145,10 @@ class DocumentWithSegmentsListResponse(ResponseModel):
page: int
class OpaqueObjectResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(
console_ns,
KnowledgeConfig,
@ -158,6 +162,7 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
BinaryFileResponse,
SimpleResultMessageResponse,
SimpleResultResponse,
UrlResponse,
@ -167,6 +172,7 @@ register_response_schema_models(
DocumentWithSegmentsResponse,
DatasetAndDocumentResponse,
DocumentWithSegmentsListResponse,
OpaqueObjectResponse,
)
@ -216,7 +222,7 @@ class GetProcessRuleApi(Resource):
@console_ns.doc("get_process_rule")
@console_ns.doc(description="Get dataset document processing rules")
@console_ns.doc(params={"document_id": "Document ID (optional)"})
@console_ns.response(200, "Process rules retrieved successfully")
@console_ns.response(200, "Process rules retrieved successfully", console_ns.models[OpaqueObjectResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -537,7 +543,11 @@ class DocumentIndexingEstimateApi(DocumentResource):
@console_ns.doc("estimate_document_indexing")
@console_ns.doc(description="Estimate document indexing cost")
@console_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
@console_ns.response(200, "Indexing estimate calculated successfully")
@console_ns.response(
200,
"Indexing estimate calculated successfully",
console_ns.models[OpaqueObjectResponse.__name__],
)
@console_ns.response(404, "Document not found")
@console_ns.response(400, "Document already finished")
@setup_required
@ -606,6 +616,11 @@ class DocumentIndexingEstimateApi(DocumentResource):
@console_ns.route("/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-estimate")
class DocumentBatchIndexingEstimateApi(DocumentResource):
@console_ns.response(
200,
"Batch indexing estimate calculated successfully",
console_ns.models[OpaqueObjectResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -824,7 +839,7 @@ class DocumentApi(DocumentResource):
"metadata": "Metadata inclusion (all/only/without)",
}
)
@console_ns.response(200, "Document retrieved successfully")
@console_ns.response(200, "Document retrieved successfully", console_ns.models[OpaqueObjectResponse.__name__])
@console_ns.response(404, "Document not found")
@setup_required
@login_required
@ -966,6 +981,7 @@ class DocumentBatchDownloadZipApi(DocumentResource):
@console_ns.doc("download_dataset_documents_as_zip")
@console_ns.doc(description="Download selected dataset documents as a single ZIP archive (upload-file only)")
@console_ns.response(200, "ZIP archive generated successfully", console_ns.models[BinaryFileResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1324,6 +1340,11 @@ class WebsiteDocumentSyncApi(DocumentResource):
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/pipeline-execution-log")
class DocumentPipelineExecutionLogApi(DocumentResource):
@console_ns.response(
200,
"Document pipeline execution log retrieved successfully",
console_ns.models[OpaqueObjectResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -1464,7 +1485,7 @@ class DocumentSummaryStatusApi(DocumentResource):
@console_ns.doc("get_document_summary_status")
@console_ns.doc(description="Get summary index generation status for a document")
@console_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
@console_ns.response(200, "Summary status retrieved successfully")
@console_ns.response(200, "Summary status retrieved successfully", console_ns.models[OpaqueObjectResponse.__name__])
@console_ns.response(404, "Document not found")
@setup_required
@login_required

View File

@ -1,13 +1,19 @@
from typing import Any
from uuid import UUID
from flask import request
from flask_restx import Resource, fields, marshal
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services
from controllers.common.fields import UsageCountResponse
from controllers.common.schema import get_or_create_model, register_response_schema_models, register_schema_models
from controllers.common.schema import (
get_or_create_model,
query_params_from_model,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.datasets.error import DatasetNameDuplicateError
from controllers.console.wraps import (
@ -17,6 +23,7 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from fields.base import ResponseModel
from fields.dataset_fields import (
dataset_detail_fields,
dataset_retrieval_model_fields,
@ -88,13 +95,15 @@ class ExternalDatasetCreatePayload(BaseModel):
external_knowledge_id: str
name: str = Field(..., min_length=1, max_length=100)
description: str | None = Field(None, max_length=400)
external_retrieval_model: dict[str, object] | None = None
external_retrieval_model: dict[str, object] | None = Field(default=None)
class ExternalHitTestingPayload(BaseModel):
query: str
external_retrieval_model: dict[str, object] | None = None
metadata_filtering_conditions: dict[str, object] | None = None
external_retrieval_model: dict[str, object] | None = Field(default=None)
metadata_filtering_conditions: dict[str, object] | None = Field(
default=None,
)
class BedrockRetrievalPayload(BaseModel):
@ -109,6 +118,34 @@ class ExternalApiTemplateListQuery(BaseModel):
keyword: str | None = Field(default=None, description="Search keyword")
class ExternalKnowledgeDatasetBindingResponse(ResponseModel):
id: str
name: str
class ExternalKnowledgeApiResponse(ResponseModel):
id: str
tenant_id: str
name: str
description: str
settings: dict[str, Any] | None = Field(default=None)
dataset_bindings: list[ExternalKnowledgeDatasetBindingResponse] = Field(default_factory=list)
created_by: str
created_at: str
class ExternalKnowledgeApiListResponse(ResponseModel):
data: list[ExternalKnowledgeApiResponse]
has_more: bool
limit: int
total: int
page: int
class ExternalRetrievalTestResponse(RootModel[dict[str, Any] | list[dict[str, Any]]]):
root: dict[str, Any] | list[dict[str, Any]]
register_schema_models(
console_ns,
ExternalKnowledgeApiPayload,
@ -117,20 +154,24 @@ register_schema_models(
BedrockRetrievalPayload,
ExternalApiTemplateListQuery,
)
register_response_schema_models(
console_ns,
ExternalKnowledgeApiResponse,
ExternalKnowledgeApiListResponse,
ExternalRetrievalTestResponse,
)
@console_ns.route("/datasets/external-knowledge-api")
class ExternalApiTemplateListApi(Resource):
@console_ns.doc("get_external_api_templates")
@console_ns.doc(description="Get external knowledge API templates")
@console_ns.doc(
params={
"page": "Page number (default: 1)",
"limit": "Number of items per page (default: 20)",
"keyword": "Search keyword",
}
@console_ns.doc(params=query_params_from_model(ExternalApiTemplateListQuery))
@console_ns.response(
200,
"External API templates retrieved successfully",
console_ns.models[ExternalKnowledgeApiListResponse.__name__],
)
@console_ns.response(200, "External API templates retrieved successfully")
@setup_required
@login_required
@with_current_tenant_id
@ -154,6 +195,11 @@ class ExternalApiTemplateListApi(Resource):
@login_required
@account_initialization_required
@console_ns.expect(console_ns.models[ExternalKnowledgeApiPayload.__name__])
@console_ns.response(
201,
"External API template created successfully",
console_ns.models[ExternalKnowledgeApiResponse.__name__],
)
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account):
@ -180,7 +226,11 @@ class ExternalApiTemplateApi(Resource):
@console_ns.doc("get_external_api_template")
@console_ns.doc(description="Get external knowledge API template details")
@console_ns.doc(params={"external_knowledge_api_id": "External knowledge API ID"})
@console_ns.response(200, "External API template retrieved successfully")
@console_ns.response(
200,
"External API template retrieved successfully",
console_ns.models[ExternalKnowledgeApiResponse.__name__],
)
@console_ns.response(404, "Template not found")
@setup_required
@login_required
@ -196,6 +246,11 @@ class ExternalApiTemplateApi(Resource):
return external_knowledge_api.to_dict(), 200
@console_ns.response(
200,
"External API template updated successfully",
console_ns.models[ExternalKnowledgeApiResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -293,7 +348,11 @@ class ExternalKnowledgeHitTestingApi(Resource):
@console_ns.doc(description="Test external knowledge retrieval for dataset")
@console_ns.doc(params={"dataset_id": "Dataset ID"})
@console_ns.expect(console_ns.models[ExternalHitTestingPayload.__name__])
@console_ns.response(200, "External hit testing completed successfully")
@console_ns.response(
200,
"External hit testing completed successfully",
console_ns.models[ExternalRetrievalTestResponse.__name__],
)
@console_ns.response(404, "Dataset not found")
@console_ns.response(400, "Invalid parameters")
@setup_required
@ -334,7 +393,11 @@ class BedrockRetrievalApi(Resource):
@console_ns.doc("bedrock_retrieval_test")
@console_ns.doc(description="Bedrock retrieval test (internal use only)")
@console_ns.expect(console_ns.models[BedrockRetrievalPayload.__name__])
@console_ns.response(200, "Bedrock retrieval test completed")
@console_ns.response(
200,
"Bedrock retrieval test completed",
console_ns.models[ExternalRetrievalTestResponse.__name__],
)
def post(self):
payload = BedrockRetrievalPayload.model_validate(console_ns.payload or {})

View File

@ -32,7 +32,7 @@ logger = logging.getLogger(__name__)
class HitTestingPayload(BaseModel):
query: str = Field(max_length=250)
retrieval_model: RetrievalModel | None = None
external_retrieval_model: dict[str, Any] | None = None
external_retrieval_model: dict[str, Any] | None = Field(default=None)
attachment_ids: list[str] | None = None

View File

@ -6,8 +6,8 @@ from pydantic import BaseModel, Field
from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.fields import RedirectResponse, 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 (
account_initialization_required,
@ -16,7 +16,9 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from core.plugin.entities.plugin_daemon import PluginOAuthAuthorizationUrlResponse
from core.plugin.impl.oauth import OAuthHandler
from fields.base import ResponseModel
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.login import login_required
@ -38,11 +40,11 @@ class DatasourceCredentialDeletePayload(BaseModel):
class DatasourceCredentialUpdatePayload(BaseModel):
credential_id: str
name: str | None = Field(default=None, max_length=100)
credentials: dict[str, Any] | None = None
credentials: dict[str, Any] | None = Field(default=None)
class DatasourceCustomClientPayload(BaseModel):
client_params: dict[str, Any] | None = None
client_params: dict[str, Any] | None = Field(default=None)
enable_oauth_custom_client: bool | None = None
@ -55,8 +57,25 @@ class DatasourceUpdateNamePayload(BaseModel):
name: str = Field(max_length=100)
class DatasourceOAuthAuthorizationQuery(BaseModel):
credential_id: str | None = Field(default=None, description="Credential ID to reauthorize")
class DatasourceOAuthCallbackQuery(BaseModel):
code: str | None = Field(default=None, description="Authorization code from OAuth provider")
state: str | None = Field(default=None, description="OAuth state parameter")
error: str | None = Field(default=None, description="Error message from OAuth provider")
context_id: str | None = Field(default=None, description="OAuth proxy context ID")
class DatasourceCredentialsResponse(ResponseModel):
result: Any
register_schema_models(
console_ns,
DatasourceOAuthAuthorizationQuery,
DatasourceOAuthCallbackQuery,
DatasourceCredentialPayload,
DatasourceCredentialDeletePayload,
DatasourceCredentialUpdatePayload,
@ -64,11 +83,23 @@ register_schema_models(
DatasourceDefaultPayload,
DatasourceUpdateNamePayload,
)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(
console_ns,
DatasourceCredentialsResponse,
PluginOAuthAuthorizationUrlResponse,
RedirectResponse,
SimpleResultResponse,
)
@console_ns.route("/oauth/plugin/<path:provider_id>/datasource/get-authorization-url")
class DatasourcePluginOAuthAuthorizationUrl(Resource):
@console_ns.doc(params=query_params_from_model(DatasourceOAuthAuthorizationQuery))
@console_ns.response(
200,
"Authorization URL retrieved successfully",
console_ns.models[PluginOAuthAuthorizationUrlResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -118,6 +149,12 @@ 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__],
)
@setup_required
def get(self, provider_id: str):
context_id = request.cookies.get("context_id") or request.args.get("context_id")
@ -176,6 +213,7 @@ 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__])
@setup_required
@login_required
@account_initialization_required
@ -200,6 +238,7 @@ class DatasourceAuth(Resource):
@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):
@ -243,6 +282,7 @@ class DatasourceAuthDeleteApi(Resource):
@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__])
@setup_required
@login_required
@account_initialization_required
@ -266,6 +306,7 @@ class DatasourceAuthUpdateApi(Resource):
@console_ns.route("/auth/plugin/datasource/list")
class DatasourceAuthListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -278,6 +319,7 @@ class DatasourceAuthListApi(Resource):
@console_ns.route("/auth/plugin/datasource/default-list")
class DatasourceHardCodeAuthListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[DatasourceCredentialsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -291,6 +333,7 @@ class DatasourceHardCodeAuthListApi(Resource):
@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__])
@setup_required
@login_required
@account_initialization_required

View File

@ -1,9 +1,11 @@
from typing import Any
from flask_restx import ( # type: ignore
Resource, # type: ignore
)
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
from controllers.common.schema import register_schema_models
from controllers.common.schema import register_response_schema_models, 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
@ -14,17 +16,23 @@ from services.rag_pipeline.rag_pipeline import RagPipelineService
class Parser(BaseModel):
inputs: dict
inputs: dict[str, Any]
datasource_type: str
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__])
@setup_required
@login_required
@account_initialization_required

View File

@ -74,7 +74,9 @@ class PipelineTemplateDetailResponse(ResponseModel):
class CustomizedPipelineTemplatePayload(BaseModel):
name: str = Field(..., min_length=1, max_length=40)
description: str = Field(default="", max_length=400)
icon_info: dict[str, object] = Field(default_factory=lambda: IconInfo(icon="").model_dump())
icon_info: dict[str, object] = Field(
default_factory=lambda: IconInfo(icon="").model_dump(),
)
register_schema_models(

View File

@ -11,13 +11,14 @@ from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.common.schema import register_schema_models
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.console import console_ns
from controllers.console.app.error import (
DraftWorkflowNotExist,
)
from controllers.console.app.workflow_draft_variable import (
_WORKFLOW_DRAFT_VARIABLE_FIELDS, # type: ignore[private-usage]
EnvironmentVariableListResponse,
workflow_draft_variable_list_model,
workflow_draft_variable_list_without_value_model,
workflow_draft_variable_model,
@ -40,14 +41,9 @@ logger = logging.getLogger(__name__)
_file_access_controller = DatabaseFileAccessController()
def _create_pagination_parser():
class PaginationQuery(BaseModel):
page: int = Field(default=1, ge=1, le=100_000)
limit: int = Field(default=20, ge=1, le=100)
register_schema_models(console_ns, PaginationQuery)
return PaginationQuery
class PaginationQuery(BaseModel):
page: int = Field(default=1, ge=1, le=100_000)
limit: int = Field(default=20, ge=1, le=100)
class WorkflowDraftVariablePatchPayload(BaseModel):
@ -55,7 +51,7 @@ class WorkflowDraftVariablePatchPayload(BaseModel):
value: Any | None = None
register_schema_models(console_ns, WorkflowDraftVariablePatchPayload)
register_schema_models(console_ns, PaginationQuery, WorkflowDraftVariablePatchPayload)
def _api_prerequisite[T, **P, R](
@ -87,14 +83,19 @@ def _api_prerequisite[T, **P, R](
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/variables")
class RagPipelineVariableCollectionApi(Resource):
@console_ns.doc(params=query_params_from_model(PaginationQuery))
@console_ns.response(
200,
"Workflow variables retrieved successfully",
workflow_draft_variable_list_without_value_model,
)
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
def get(self, current_user: Account, pipeline: Pipeline):
"""
Get draft workflow
"""
pagination = _create_pagination_parser()
query = pagination.model_validate(request.args.to_dict())
query = PaginationQuery.model_validate(request.args.to_dict())
# fetch draft workflow by app_model
rag_pipeline_service = RagPipelineService()
@ -116,6 +117,7 @@ class RagPipelineVariableCollectionApi(Resource):
return workflow_vars
@console_ns.response(204, "Workflow variables deleted successfully")
@_api_prerequisite
def delete(self, current_user: Account, pipeline: Pipeline):
draft_var_srv = WorkflowDraftVariableService(
@ -146,6 +148,7 @@ def validate_node_id(node_id: str) -> NoReturn | None:
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/nodes/<string:node_id>/variables")
class RagPipelineNodeVariableCollectionApi(Resource):
@console_ns.response(200, "Node variables retrieved successfully", workflow_draft_variable_list_model)
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_model)
def get(self, current_user: Account, pipeline: Pipeline, node_id: str):
@ -158,6 +161,7 @@ class RagPipelineNodeVariableCollectionApi(Resource):
return node_vars
@console_ns.response(204, "Node variables deleted successfully")
@_api_prerequisite
def delete(self, current_user: Account, pipeline: Pipeline, node_id: str):
validate_node_id(node_id)
@ -172,6 +176,7 @@ class RagPipelineVariableApi(Resource):
_PATCH_NAME_FIELD = "name"
_PATCH_VALUE_FIELD = "value"
@console_ns.response(200, "Variable retrieved successfully", workflow_draft_variable_model)
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
def get(self, _current_user: Account, pipeline: Pipeline, variable_id: UUID):
@ -186,6 +191,7 @@ class RagPipelineVariableApi(Resource):
raise NotFoundError(description=f"variable not found, id={variable_id_str}")
return variable
@console_ns.response(200, "Variable updated successfully", workflow_draft_variable_model)
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
@console_ns.expect(console_ns.models[WorkflowDraftVariablePatchPayload.__name__])
@ -257,6 +263,7 @@ class RagPipelineVariableApi(Resource):
db.session.commit()
return variable
@console_ns.response(204, "Variable deleted successfully")
@_api_prerequisite
def delete(self, _current_user: Account, pipeline: Pipeline, variable_id: UUID):
draft_var_srv = WorkflowDraftVariableService(
@ -275,6 +282,8 @@ class RagPipelineVariableApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/variables/<uuid:variable_id>/reset")
class RagPipelineVariableResetApi(Resource):
@console_ns.response(200, "Variable reset successfully", workflow_draft_variable_model)
@console_ns.response(204, "Variable reset (no content)")
@_api_prerequisite
def put(self, _current_user: Account, pipeline: Pipeline, variable_id: UUID):
draft_var_srv = WorkflowDraftVariableService(
@ -318,6 +327,7 @@ def _get_variable_list(pipeline: Pipeline, node_id: str, current_user_id: str) -
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/system-variables")
class RagPipelineSystemVariableCollectionApi(Resource):
@console_ns.response(200, "System variables retrieved successfully", workflow_draft_variable_list_model)
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_model)
def get(self, current_user: Account, pipeline: Pipeline):
@ -326,6 +336,11 @@ class RagPipelineSystemVariableCollectionApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/environment-variables")
class RagPipelineEnvironmentVariableCollectionApi(Resource):
@console_ns.response(
200,
"Environment variables retrieved successfully",
console_ns.models[EnvironmentVariableListResponse.__name__],
)
@_api_prerequisite
def get(self, _current_user: Account, pipeline: Pipeline):
"""

View File

@ -5,14 +5,14 @@ from uuid import UUID
from flask import abort, request
from flask_restx import Resource
from pydantic import BaseModel, Field, ValidationError
from pydantic import BaseModel, Field, RootModel, ValidationError
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
import services
from controllers.common.controller_schemas import DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
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.app.error import (
ConversationCompletedError,
@ -21,6 +21,8 @@ from controllers.console.app.error import (
)
from controllers.console.app.workflow import (
RESTORE_SOURCE_WORKFLOW_MUST_BE_PUBLISHED_MESSAGE,
DefaultBlockConfigResponse,
DefaultBlockConfigsResponse,
WorkflowPaginationResponse,
WorkflowResponse,
)
@ -67,14 +69,14 @@ logger = logging.getLogger(__name__)
class DraftWorkflowSyncPayload(BaseModel):
graph: dict[str, Any]
hash: str | None = None
environment_variables: list[dict[str, Any]] | None = None
conversation_variables: list[dict[str, Any]] | None = None
rag_pipeline_variables: list[dict[str, Any]] | None = None
features: dict[str, Any] | None = None
environment_variables: list[dict[str, Any]] | None = Field(default=None)
conversation_variables: list[dict[str, Any]] | None = Field(default=None)
rag_pipeline_variables: list[dict[str, Any]] | None = Field(default=None)
features: dict[str, Any] | None = Field(default=None)
class NodeRunPayload(BaseModel):
inputs: dict[str, Any] | None = None
inputs: dict[str, Any] | None = Field(default=None)
class NodeRunRequiredPayload(BaseModel):
@ -131,6 +133,14 @@ class RagPipelineWorkflowPublishResponse(ResponseModel):
created_at: int
class RagPipelineOpaqueResponse(RootModel[Any]):
root: Any
class RagPipelineStepParametersResponse(ResponseModel):
variables: Any
register_schema_models(
console_ns,
DraftWorkflowSyncPayload,
@ -149,6 +159,10 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
DefaultBlockConfigResponse,
DefaultBlockConfigsResponse,
RagPipelineOpaqueResponse,
RagPipelineStepParametersResponse,
RagPipelineWorkflowPublishResponse,
RagPipelineWorkflowSyncResponse,
SimpleResultResponse,
@ -192,6 +206,7 @@ class DraftRagPipelineApi(Resource):
@with_current_user
@get_rag_pipeline
@edit_permission_required
@console_ns.expect(console_ns.models[DraftWorkflowSyncPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineWorkflowSyncResponse.__name__])
def post(self, current_user: Account, pipeline: Pipeline):
"""
@ -244,6 +259,7 @@ class DraftRagPipelineApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/iteration/nodes/<string:node_id>/run")
class RagPipelineDraftRunIterationNodeApi(Resource):
@console_ns.expect(console_ns.models[NodeRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -277,6 +293,7 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/loop/nodes/<string:node_id>/run")
class RagPipelineDraftRunLoopNodeApi(Resource):
@console_ns.expect(console_ns.models[NodeRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -310,6 +327,7 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/run")
class DraftRagPipelineRunApi(Resource):
@console_ns.expect(console_ns.models[DraftWorkflowRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -340,6 +358,7 @@ class DraftRagPipelineRunApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/run")
class PublishedRagPipelineRunApi(Resource):
@console_ns.expect(console_ns.models[PublishedWorkflowRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -371,6 +390,7 @@ class PublishedRagPipelineRunApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/datasource/nodes/<string:node_id>/run")
class RagPipelinePublishedDatasourceNodeRunApi(Resource):
@console_ns.expect(console_ns.models[DatasourceNodeRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -402,6 +422,7 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/datasource/nodes/<string:node_id>/run")
class RagPipelineDraftDatasourceNodeRunApi(Resource):
@console_ns.expect(console_ns.models[DatasourceNodeRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -541,6 +562,11 @@ class PublishedRagPipelineApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/default-workflow-block-configs")
class DefaultRagPipelineBlockConfigsApi(Resource):
@console_ns.response(
200,
"Default block configs retrieved successfully",
console_ns.models[DefaultBlockConfigsResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -557,6 +583,12 @@ class DefaultRagPipelineBlockConfigsApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/default-workflow-block-configs/<string:block_type>")
class DefaultRagPipelineBlockConfigApi(Resource):
@console_ns.doc(params=query_params_from_model(DefaultBlockConfigQuery))
@console_ns.response(
200,
"Default block config retrieved successfully",
console_ns.models[DefaultBlockConfigResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -582,6 +614,7 @@ class DefaultRagPipelineBlockConfigApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows")
class PublishedAllRagPipelineApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkflowListQuery))
@console_ns.response(
200,
"Published workflows retrieved successfully",
@ -673,6 +706,7 @@ class RagPipelineByIdApi(Resource):
@edit_permission_required
@with_current_user
@get_rag_pipeline
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
def patch(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
"""
Update workflow attributes
@ -734,6 +768,8 @@ class RagPipelineByIdApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/processing/parameters")
class PublishedRagPipelineSecondStepApi(Resource):
@console_ns.doc(params=query_params_from_model(NodeIdQuery))
@console_ns.response(200, "Success", console_ns.models[RagPipelineStepParametersResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -754,6 +790,8 @@ class PublishedRagPipelineSecondStepApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/published/pre-processing/parameters")
class PublishedRagPipelineFirstStepApi(Resource):
@console_ns.doc(params=query_params_from_model(NodeIdQuery))
@console_ns.response(200, "Success", console_ns.models[RagPipelineStepParametersResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -774,6 +812,8 @@ class PublishedRagPipelineFirstStepApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/pre-processing/parameters")
class DraftRagPipelineFirstStepApi(Resource):
@console_ns.doc(params=query_params_from_model(NodeIdQuery))
@console_ns.response(200, "Success", console_ns.models[RagPipelineStepParametersResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -794,6 +834,8 @@ class DraftRagPipelineFirstStepApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflows/draft/processing/parameters")
class DraftRagPipelineSecondStepApi(Resource):
@console_ns.doc(params=query_params_from_model(NodeIdQuery))
@console_ns.response(200, "Success", console_ns.models[RagPipelineStepParametersResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -815,6 +857,7 @@ class DraftRagPipelineSecondStepApi(Resource):
@console_ns.route("/rag/pipelines/<uuid:pipeline_id>/workflow-runs")
class RagPipelineWorkflowRunListApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkflowRunQuery))
@console_ns.response(
200,
"Workflow runs retrieved successfully",
@ -903,6 +946,7 @@ class RagPipelineWorkflowRunNodeExecutionListApi(Resource):
@console_ns.route("/rag/pipelines/datasource-plugins")
class DatasourceListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -939,6 +983,7 @@ class RagPipelineWorkflowLastRunApi(Resource):
@console_ns.route("/rag/pipelines/transform/datasets/<uuid:dataset_id>")
class RagPipelineTransformApi(Resource):
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -986,6 +1031,8 @@ class RagPipelineDatasourceVariableApi(Resource):
@console_ns.route("/rag/pipelines/recommended-plugins")
class RagPipelineRecommendedPluginApi(Resource):
@console_ns.doc(params=query_params_from_model(RagPipelineRecommendedPluginQuery))
@console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -1,10 +1,10 @@
from typing import Literal
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
from controllers.common.schema import query_params_from_model, register_schema_models
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.datasets.error import WebsiteCrawlError
from controllers.console.wraps import account_initialization_required, setup_required
@ -22,7 +22,12 @@ class WebsiteCrawlStatusQuery(BaseModel):
provider: Literal["firecrawl", "watercrawl", "jinareader"]
class WebsiteCrawlResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(console_ns, WebsiteCrawlPayload, WebsiteCrawlStatusQuery)
register_response_schema_models(console_ns, WebsiteCrawlResponse)
@console_ns.route("/website/crawl")
@ -30,7 +35,7 @@ class WebsiteCrawlApi(Resource):
@console_ns.doc("crawl_website")
@console_ns.doc(description="Crawl website content")
@console_ns.expect(console_ns.models[WebsiteCrawlPayload.__name__])
@console_ns.response(200, "Website crawl initiated successfully")
@console_ns.response(200, "Website crawl initiated successfully", console_ns.models[WebsiteCrawlResponse.__name__])
@console_ns.response(400, "Invalid crawl parameters")
@setup_required
@login_required
@ -58,7 +63,7 @@ class WebsiteCrawlStatusApi(Resource):
@console_ns.doc(description="Get website crawl status")
@console_ns.doc(params={"job_id": "Crawl job ID", "provider": "Crawl provider (firecrawl/watercrawl/jinareader)"})
@console_ns.doc(params=query_params_from_model(WebsiteCrawlStatusQuery))
@console_ns.response(200, "Crawl status retrieved successfully")
@console_ns.response(200, "Crawl status retrieved successfully", console_ns.models[WebsiteCrawlResponse.__name__])
@console_ns.response(404, "Crawl job not found")
@console_ns.response(400, "Invalid provider")
@setup_required

View File

@ -5,7 +5,8 @@ from werkzeug.exceptions import InternalServerError
import services
from controllers.common.controller_schemas import TextToAudioPayload
from controllers.common.schema import register_schema_model
from controllers.common.fields import AudioBinaryResponse, AudioTranscriptResponse
from controllers.common.schema import register_response_schema_models, register_schema_model
from controllers.console.app.error import (
AppUnavailableError,
AudioTooLargeError,
@ -34,6 +35,7 @@ from .. import console_ns
logger = logging.getLogger(__name__)
register_schema_model(console_ns, TextToAudioPayload)
register_response_schema_models(console_ns, AudioBinaryResponse, AudioTranscriptResponse)
@console_ns.route(
@ -41,6 +43,7 @@ register_schema_model(console_ns, TextToAudioPayload)
endpoint="installed_app_audio",
)
class ChatAudioApi(InstalledAppResource):
@console_ns.response(200, "Success", console_ns.models[AudioTranscriptResponse.__name__])
def post(self, installed_app: InstalledApp):
app_model = installed_app.app
if app_model is None:
@ -84,6 +87,7 @@ class ChatAudioApi(InstalledAppResource):
)
class ChatTextApi(InstalledAppResource):
@console_ns.expect(console_ns.models[TextToAudioPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__])
def post(self, installed_app: InstalledApp):
app_model = installed_app.app
if app_model is None:

View File

@ -1,17 +1,44 @@
from typing import Any, cast
from flask import request
from flask_restx import Resource
from flask_restx import Namespace, Resource
from pydantic import BaseModel, Field, RootModel
from sqlalchemy import select
from controllers.common.schema import query_params_from_model, register_response_schema_models
from controllers.console import api
from controllers.console.explore.wraps import explore_banner_enabled
from extensions.ext_database import db
from fields.base import ResponseModel
from models.enums import BannerStatus
from models.model import ExporleBanner
class BannerListQuery(BaseModel):
language: str = Field(default="en-US", description="Banner language")
class BannerResponse(ResponseModel):
id: str
content: Any
link: str | None = None
sort: int
status: str
created_at: str | None = None
class BannerListResponse(RootModel[list[BannerResponse]]):
root: list[BannerResponse]
register_response_schema_models(cast(Namespace, api), BannerListResponse)
class BannerApi(Resource):
"""Resource for banner list."""
@api.doc(params=query_params_from_model(BannerListQuery))
@api.response(200, "Success", api.models[BannerListResponse.__name__])
@explore_banner_enabled
def get(self):
"""Get banner list."""

View File

@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import InternalServerError, NotFound
import services
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console.app.error import (
AppUnavailableError,
@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class CompletionMessageExplorePayload(BaseModel):
inputs: dict[str, Any]
query: str = ""
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
response_mode: Literal["blocking", "streaming"] | None = None
retriever_from: str = Field(default="explore_app")
@ -52,7 +52,7 @@ class CompletionMessageExplorePayload(BaseModel):
class ChatMessagePayload(BaseModel):
inputs: dict[str, Any]
query: str
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
conversation_id: str | None = None
parent_message_id: str | None = None
retriever_from: str = Field(default="explore_app")
@ -73,7 +73,7 @@ class ChatMessagePayload(BaseModel):
register_schema_models(console_ns, CompletionMessageExplorePayload, ChatMessagePayload)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(console_ns, GeneratedAppResponse, SimpleResultResponse)
# define completion api for user
@ -83,6 +83,7 @@ register_response_schema_models(console_ns, SimpleResultResponse)
)
class CompletionApi(InstalledAppResource):
@console_ns.expect(console_ns.models[CompletionMessageExplorePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app
@ -158,6 +159,7 @@ class CompletionStopApi(InstalledAppResource):
)
class ChatApi(InstalledAppResource):
@console_ns.expect(console_ns.models[ChatMessagePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app

View File

@ -36,7 +36,12 @@ class ConversationListQuery(BaseModel):
register_schema_models(console_ns, ConversationListQuery, ConversationRenamePayload)
register_response_schema_models(console_ns, ResultResponse)
register_response_schema_models(
console_ns,
ConversationInfiniteScrollPagination,
ResultResponse,
SimpleConversation,
)
@console_ns.route(
@ -45,6 +50,7 @@ register_response_schema_models(console_ns, ResultResponse)
)
class ConversationListApi(InstalledAppResource):
@console_ns.doc(params=query_params_from_model(ConversationListQuery))
@console_ns.response(200, "Success", console_ns.models[ConversationInfiniteScrollPagination.__name__])
@with_current_user
def get(self, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app
@ -118,6 +124,7 @@ class ConversationApi(InstalledAppResource):
)
class ConversationRenameApi(InstalledAppResource):
@console_ns.expect(console_ns.models[ConversationRenamePayload.__name__])
@console_ns.response(200, "Conversation renamed successfully", console_ns.models[SimpleConversation.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp, c_id: UUID):
app_model = installed_app.app

View File

@ -9,7 +9,7 @@ from sqlalchemy import and_, exists, or_, select
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from controllers.common.fields import SimpleMessageResponse, SimpleResultMessageResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
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.explore.wraps import InstalledAppResource
from controllers.console.wraps import (
@ -153,6 +153,7 @@ register_response_schema_models(console_ns, SimpleMessageResponse, SimpleResultM
class InstalledAppsListApi(Resource):
@login_required
@account_initialization_required
@console_ns.doc(params=query_params_from_model(InstalledAppsListQuery))
@console_ns.response(200, "Success", console_ns.models[InstalledAppListResponse.__name__])
@with_current_user
@with_current_tenant_id
@ -234,6 +235,7 @@ class InstalledAppsListApi(Resource):
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("apps")
@console_ns.expect(console_ns.models[InstalledAppCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleMessageResponse.__name__])
@with_current_tenant_id
def post(self, current_tenant_id: str):
@ -295,6 +297,7 @@ class InstalledAppApi(InstalledAppResource):
return "", 204
@console_ns.response(200, "Success", console_ns.models[SimpleResultMessageResponse.__name__])
@console_ns.expect(console_ns.models[InstalledAppUpdatePayload.__name__])
def patch(self, installed_app: InstalledApp):
payload = InstalledAppUpdatePayload.model_validate(console_ns.payload or {})

View File

@ -7,6 +7,7 @@ from pydantic import BaseModel, TypeAdapter
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery
from controllers.common.fields import GeneratedAppResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console.app.error import (
AppMoreLikeThisDisabledError,
@ -52,7 +53,13 @@ class MoreLikeThisQuery(BaseModel):
register_schema_models(console_ns, MessageListQuery, MessageFeedbackPayload, MoreLikeThisQuery)
register_response_schema_models(console_ns, ResultResponse, SuggestedQuestionsResponse)
register_response_schema_models(
console_ns,
GeneratedAppResponse,
MessageInfiniteScrollPagination,
ResultResponse,
SuggestedQuestionsResponse,
)
@console_ns.route(
@ -61,6 +68,7 @@ register_response_schema_models(console_ns, ResultResponse, SuggestedQuestionsRe
)
class MessageListApi(InstalledAppResource):
@console_ns.doc(params=query_params_from_model(MessageListQuery))
@console_ns.response(200, "Success", console_ns.models[MessageInfiniteScrollPagination.__name__])
@with_current_user
def get(self, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app
@ -130,6 +138,7 @@ class MessageFeedbackApi(InstalledAppResource):
)
class MessageMoreLikeThisApi(InstalledAppResource):
@console_ns.doc(params=query_params_from_model(MoreLikeThisQuery))
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@with_current_user
def get(self, current_user: Account, installed_app: InstalledApp, message_id: UUID):
app_model = installed_app.app

View File

@ -1,6 +1,9 @@
from typing import Any, cast
from pydantic import BaseModel, Field
from controllers.common import fields
from controllers.common.schema import register_response_schema_models
from controllers.console import console_ns
from controllers.console.app.error import AppUnavailableError
from controllers.console.explore.wraps import InstalledAppResource
@ -9,10 +12,18 @@ from models.model import AppMode, InstalledApp
from services.app_service import AppService
class ExploreAppMetaResponse(BaseModel):
tool_icons: dict[str, Any] = Field(default_factory=dict)
register_response_schema_models(console_ns, fields.Parameters, ExploreAppMetaResponse)
@console_ns.route("/installed-apps/<uuid:installed_app_id>/parameters", endpoint="installed_app_parameters")
class AppParameterApi(InstalledAppResource):
"""Resource for app variables."""
@console_ns.response(200, "Success", console_ns.models[fields.Parameters.__name__])
def get(self, installed_app: InstalledApp):
"""Retrieve app parameters."""
app_model = installed_app.app
@ -42,6 +53,7 @@ class AppParameterApi(InstalledAppResource):
@console_ns.route("/installed-apps/<uuid:installed_app_id>/meta", endpoint="installed_app_meta")
class ExploreAppMetaApi(InstalledAppResource):
@console_ns.response(200, "Success", console_ns.models[ExploreAppMetaResponse.__name__])
def get(self, installed_app: InstalledApp):
"""Get app meta"""
app_model = installed_app.app

View File

@ -3,10 +3,10 @@ from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, computed_field, field_validator
from pydantic import BaseModel, Field, RootModel, computed_field, field_validator
from constants.languages import languages
from controllers.common.schema import query_params_from_model, register_schema_models
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 account_initialization_required, with_current_user
from fields.base import ResponseModel
@ -65,6 +65,10 @@ class RecommendedAppListResponse(ResponseModel):
categories: list[str]
class RecommendedAppDetailResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(
console_ns,
RecommendedAppsQuery,
@ -72,6 +76,7 @@ register_schema_models(
RecommendedAppResponse,
RecommendedAppListResponse,
)
register_response_schema_models(console_ns, RecommendedAppDetailResponse)
@console_ns.route("/explore/apps")
@ -100,6 +105,7 @@ class RecommendedAppListApi(Resource):
@console_ns.route("/explore/apps/<uuid:app_id>")
class RecommendedAppApi(Resource):
@console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailResponse.__name__])
@login_required
@account_initialization_required
def get(self, app_id: UUID):

View File

@ -19,12 +19,13 @@ from services.errors.message import MessageNotExistsError
from services.saved_message_service import SavedMessageService
register_schema_models(console_ns, SavedMessageListQuery, SavedMessageCreatePayload)
register_response_schema_models(console_ns, ResultResponse)
register_response_schema_models(console_ns, ResultResponse, SavedMessageInfiniteScrollPagination)
@console_ns.route("/installed-apps/<uuid:installed_app_id>/saved-messages", endpoint="installed_app_saved_messages")
class SavedMessageListApi(InstalledAppResource):
@console_ns.doc(params=query_params_from_model(SavedMessageListQuery))
@console_ns.response(200, "Success", console_ns.models[SavedMessageInfiniteScrollPagination.__name__])
@with_current_user
def get(self, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app

View File

@ -3,14 +3,25 @@ from typing import Any, Literal, cast
from flask import request
from flask_restx import Resource, fields, marshal, marshal_with
from pydantic import BaseModel
from pydantic import BaseModel, Field
from sqlalchemy import select
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services
from controllers.common.fields import (
AudioBinaryResponse,
AudioTranscriptResponse,
GeneratedAppResponse,
SimpleResultResponse,
)
from controllers.common.fields import Parameters as ParametersResponse
from controllers.common.fields import Site as SiteResponse
from controllers.common.schema import get_or_create_model, register_schema_models
from controllers.common.schema import (
get_or_create_model,
query_params_from_model,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.app.error import (
AppUnavailableError,
@ -54,6 +65,7 @@ from fields.app_fields import (
)
from fields.dataset_fields import dataset_fields
from fields.member_fields import simple_account_fields
from fields.message_fields import SuggestedQuestionsResponse
from fields.workflow_fields import (
conversation_variable_fields,
pipeline_variable_fields,
@ -119,16 +131,28 @@ workflow_fields_copy["conversation_variables"] = fields.List(fields.Nested(conve
workflow_fields_copy["rag_pipeline_variables"] = fields.List(fields.Nested(pipeline_variable_model))
workflow_model = get_or_create_model("TrialWorkflow", workflow_fields_copy)
dataset_model = get_or_create_model("TrialDataset", dataset_fields)
dataset_list_model = get_or_create_model(
"TrialDatasetList",
{
"data": fields.List(fields.Nested(dataset_model)),
"has_more": fields.Boolean,
"limit": fields.Integer,
"total": fields.Integer,
"page": fields.Integer,
},
)
class WorkflowRunRequest(BaseModel):
inputs: dict
files: list | None = None
files: list | None = Field(default=None)
class ChatRequest(BaseModel):
inputs: dict
query: str
files: list | None = None
files: list | None = Field(default=None)
conversation_id: str | None = None
parent_message_id: str | None = None
retriever_from: str = "explore_app"
@ -144,17 +168,41 @@ class TextToSpeechRequest(BaseModel):
class CompletionRequest(BaseModel):
inputs: dict
query: str = ""
files: list | None = None
files: list | None = Field(default=None)
response_mode: Literal["blocking", "streaming"] | None = None
retriever_from: str = "explore_app"
register_schema_models(console_ns, WorkflowRunRequest, ChatRequest, TextToSpeechRequest, CompletionRequest)
class TrialDatasetListQuery(BaseModel):
page: int = Field(default=1, ge=1, description="Page number")
limit: int = Field(default=20, ge=1, description="Number of items per page")
ids: list[str] = Field(default_factory=list, description="Dataset IDs")
register_schema_models(
console_ns,
WorkflowRunRequest,
ChatRequest,
TextToSpeechRequest,
CompletionRequest,
TrialDatasetListQuery,
)
register_response_schema_models(
console_ns,
ParametersResponse,
AudioBinaryResponse,
AudioTranscriptResponse,
GeneratedAppResponse,
SimpleResultResponse,
SiteResponse,
SuggestedQuestionsResponse,
)
class TrialAppWorkflowRunApi(TrialAppResource):
@trial_feature_enable
@console_ns.expect(console_ns.models[WorkflowRunRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@with_current_user
def post(self, current_user: Account, trial_app):
"""
@ -195,6 +243,7 @@ class TrialAppWorkflowRunApi(TrialAppResource):
class TrialAppWorkflowTaskStopApi(TrialAppResource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@trial_feature_enable
def post(self, trial_app, task_id: str):
"""
@ -219,6 +268,7 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource):
class TrialChatApi(TrialAppResource):
@console_ns.expect(console_ns.models[ChatRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@trial_feature_enable
@with_current_user
def post(self, current_user: Account, trial_app):
@ -273,6 +323,7 @@ class TrialChatApi(TrialAppResource):
class TrialMessageSuggestedQuestionApi(TrialAppResource):
@console_ns.response(200, "Success", console_ns.models[SuggestedQuestionsResponse.__name__])
@with_current_user
def get(self, current_user: Account, trial_app, message_id):
app_model = trial_app
@ -308,6 +359,7 @@ class TrialMessageSuggestedQuestionApi(TrialAppResource):
class TrialChatAudioApi(TrialAppResource):
@console_ns.response(200, "Success", console_ns.models[AudioTranscriptResponse.__name__])
@trial_feature_enable
@with_current_user
def post(self, current_user: Account, trial_app):
@ -351,6 +403,7 @@ class TrialChatAudioApi(TrialAppResource):
class TrialChatTextApi(TrialAppResource):
@console_ns.expect(console_ns.models[TextToSpeechRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__])
@trial_feature_enable
@with_current_user
def post(self, current_user: Account, trial_app):
@ -397,6 +450,7 @@ class TrialChatTextApi(TrialAppResource):
class TrialCompletionApi(TrialAppResource):
@console_ns.expect(console_ns.models[CompletionRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@trial_feature_enable
@with_current_user
def post(self, current_user: Account, trial_app):
@ -446,6 +500,7 @@ class TrialCompletionApi(TrialAppResource):
class TrialSitApi(Resource):
"""Resource for trial app sites."""
@console_ns.response(200, "Success", console_ns.models[SiteResponse.__name__])
@get_app_model_with_trial(None)
def get(self, app_model):
"""Retrieve app site info.
@ -467,6 +522,7 @@ class TrialSitApi(Resource):
class TrialAppParameterApi(Resource):
"""Resource for app variables."""
@console_ns.response(200, "Success", console_ns.models[ParametersResponse.__name__])
@get_app_model_with_trial(None)
def get(self, app_model):
"""Retrieve app parameters."""
@ -495,6 +551,7 @@ class TrialAppParameterApi(Resource):
class AppApi(Resource):
@console_ns.response(200, "Success", app_detail_with_site_model)
@get_app_model_with_trial(None)
@marshal_with(app_detail_with_site_model)
def get(self, app_model):
@ -507,6 +564,7 @@ class AppApi(Resource):
class AppWorkflowApi(Resource):
@console_ns.response(200, "Success", workflow_model)
@get_app_model_with_trial(None)
@marshal_with(workflow_model)
def get(self, app_model):
@ -519,6 +577,8 @@ class AppWorkflowApi(Resource):
class DatasetListApi(Resource):
@console_ns.doc(params=query_params_from_model(TrialDatasetListQuery))
@console_ns.response(200, "Success", dataset_list_model)
@get_app_model_with_trial(None)
def get(self, app_model):
page = request.args.get("page", default=1, type=int)

View File

@ -3,7 +3,7 @@ import logging
from werkzeug.exceptions import InternalServerError
from controllers.common.controller_schemas import WorkflowRunPayload
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_model
from controllers.console.app.error import (
CompletionRequestError,
@ -36,12 +36,13 @@ from .. import console_ns
logger = logging.getLogger(__name__)
register_schema_model(console_ns, WorkflowRunPayload)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(console_ns, GeneratedAppResponse, SimpleResultResponse)
@console_ns.route("/installed-apps/<uuid:installed_app_id>/workflows/run")
class InstalledAppWorkflowRunApi(InstalledAppResource):
@console_ns.expect(console_ns.models[WorkflowRunPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[GeneratedAppResponse.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp):
"""

View File

@ -14,7 +14,7 @@ from models.api_based_extension import APIBasedExtension
from services.api_based_extension_service import APIBasedExtensionService
from services.code_based_extension_service import CodeBasedExtensionService
from ..common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_models
from ..common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, query_params_from_model, register_schema_models
from . import console_ns
from .wraps import account_initialization_required, setup_required, with_current_tenant_id
@ -60,7 +60,13 @@ class APIBasedExtensionResponse(ResponseModel):
return to_timestamp(value)
register_schema_models(console_ns, APIBasedExtensionPayload, CodeBasedExtensionResponse, APIBasedExtensionResponse)
register_schema_models(
console_ns,
CodeBasedExtensionQuery,
APIBasedExtensionPayload,
CodeBasedExtensionResponse,
APIBasedExtensionResponse,
)
console_ns.schema_model(
"APIBasedExtensionListResponse",
TypeAdapter(list[APIBasedExtensionResponse]).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
@ -90,7 +96,7 @@ def _serialize_saved_api_based_extension(extension: APIBasedExtension, api_key:
class CodeBasedExtensionAPI(Resource):
@console_ns.doc("get_code_based_extension")
@console_ns.doc(description="Get code-based extension data by module name")
@console_ns.doc(params={"module": "Extension module name"})
@console_ns.doc(params=query_params_from_model(CodeBasedExtensionQuery))
@console_ns.response(
200,
"Success",

View File

@ -5,15 +5,18 @@ Console/Studio Human Input Form APIs.
import json
import logging
from collections.abc import Generator
from typing import Any
from flask import Response, jsonify, request
from flask_restx import Resource
from pydantic import RootModel
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.common.fields import EventStreamResponse
from controllers.common.human_input import HumanInputFormSubmitPayload
from controllers.common.schema import register_schema_models
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
@ -40,7 +43,22 @@ from services.workflow_event_snapshot_service import build_workflow_event_stream
logger = logging.getLogger(__name__)
class ConsoleHumanInputFormDefinitionResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class ConsoleHumanInputFormSubmitResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(console_ns, HumanInputFormSubmitPayload)
register_response_schema_models(
console_ns,
ConsoleHumanInputFormDefinitionResponse,
ConsoleHumanInputFormSubmitResponse,
EventStreamResponse,
)
def _jsonify_form_definition(form: Form) -> Response:
@ -67,6 +85,7 @@ class ConsoleHumanInputFormApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[ConsoleHumanInputFormDefinitionResponse.__name__])
@with_current_tenant_id
def get(self, current_tenant_id: str, form_token: str):
"""
@ -89,6 +108,7 @@ class ConsoleHumanInputFormApi(Resource):
@with_current_tenant_id
@model_validate(HumanInputFormSubmitPayload)
@console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ConsoleHumanInputFormSubmitResponse.__name__])
def post(
self,
payload: HumanInputFormSubmitPayload,
@ -136,6 +156,7 @@ class ConsoleHumanInputFormApi(Resource):
class ConsoleWorkflowEventsApi(Resource):
"""Console API for getting workflow execution events after resume."""
@console_ns.response(200, "SSE event stream", console_ns.models[EventStreamResponse.__name__])
@account_initialization_required
@login_required
@with_current_user

View File

@ -6,7 +6,7 @@ from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
@ -14,6 +14,7 @@ from controllers.console.wraps import (
setup_required,
with_current_user,
)
from fields.base import ResponseModel
from libs.login import login_required
from models import Account
from services.billing_service import BillingService
@ -56,7 +57,23 @@ class DismissNotificationPayload(BaseModel):
notification_id: str = Field(...)
register_response_schema_models(console_ns, SimpleResultResponse)
class NotificationItemResponse(ResponseModel):
notification_id: str | None = None
frequency: str | None = None
lang: str
title: str
subtitle: str
body: str
title_pic_url: str
class NotificationResponse(ResponseModel):
should_show: bool
notifications: list[NotificationItemResponse]
register_schema_models(console_ns, DismissNotificationPayload)
register_response_schema_models(console_ns, SimpleResultResponse, NotificationResponse)
@console_ns.route("/notification")
@ -74,6 +91,7 @@ class NotificationApi(Resource):
401: "Unauthorized",
},
)
@console_ns.response(200, "Success", console_ns.models[NotificationResponse.__name__])
@setup_required
@login_required
@with_current_user
@ -121,6 +139,7 @@ class NotificationDismissApi(Resource):
@with_current_user
@account_initialization_required
@only_edition_cloud
@console_ns.expect(console_ns.models[DismissNotificationPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
def post(self, current_user: Account):
payload = DismissNotificationPayload.model_validate(request.get_json())

View File

@ -76,7 +76,7 @@ class CreateSnippetPayload(BaseModel):
description: str | None = Field(default=None, max_length=2000)
type: Literal["node", "group"] = "node"
icon_info: IconInfo | None = None
graph: dict[str, Any] | None = None
graph: dict[str, Any] | None = Field(default=None)
input_fields: list[InputFieldDefinition] | None = Field(default_factory=list)
@ -97,7 +97,7 @@ class SnippetDraftSyncPayload(BaseModel):
default=None,
description="Ignored. Snippet workflows do not persist conversation variables.",
)
input_fields: list[dict[str, Any]] | None = None
input_fields: list[dict[str, Any]] | None = Field(default=None)
class SnippetWorkflowListQuery(BaseModel):
@ -118,7 +118,7 @@ class SnippetDraftRunPayload(BaseModel):
"""Payload for running snippet draft workflow."""
inputs: dict[str, Any]
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
class SnippetDraftNodeRunPayload(BaseModel):
@ -126,25 +126,25 @@ class SnippetDraftNodeRunPayload(BaseModel):
inputs: dict[str, Any]
query: str = ""
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
class SnippetIterationNodeRunPayload(BaseModel):
"""Payload for running an iteration node in snippet draft workflow."""
inputs: dict[str, Any] | None = None
inputs: dict[str, Any] | None = Field(default=None)
class SnippetLoopNodeRunPayload(BaseModel):
"""Payload for running a loop node in snippet draft workflow."""
inputs: dict[str, Any] | None = None
inputs: dict[str, Any] | None = Field(default=None)
class PublishWorkflowPayload(BaseModel):
"""Payload for publishing snippet workflow."""
knowledge_base_setting: dict[str, Any] | None = None
knowledge_base_setting: dict[str, Any] | None = Field(default=None)
class SnippetImportPayload(BaseModel):

View File

@ -4,17 +4,21 @@ from functools import wraps
from flask import request
from flask_restx import Resource
from pydantic import Field
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session, sessionmaker
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from controllers.common.fields import GeneratedAppResponse, 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.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync
from controllers.console.app.workflow import (
RESTORE_SOURCE_WORKFLOW_MUST_BE_PUBLISHED_MESSAGE,
DefaultBlockConfigsResponse,
WorkflowPaginationResponse,
WorkflowPublishResponse,
WorkflowResponse,
WorkflowRestoreResponse,
)
from controllers.console.snippets.payloads import (
PublishWorkflowPayload,
@ -69,6 +73,10 @@ class SnippetWorkflowResponse(WorkflowResponse):
input_fields: list[dict] = Field(default_factory=list)
class SnippetDraftConfigResponse(BaseModel):
parallel_depth_limit: int
register_schema_models(
console_ns,
SnippetDraftSyncPayload,
@ -82,8 +90,14 @@ register_schema_models(
)
register_response_schema_models(
console_ns,
DefaultBlockConfigsResponse,
GeneratedAppResponse,
SimpleResultResponse,
SnippetDraftConfigResponse,
SnippetWorkflowResponse,
WorkflowPublishResponse,
WorkflowPaginationResponse,
WorkflowRestoreResponse,
WorkflowRunPaginationResponse,
WorkflowRunDetailResponse,
WorkflowRunNodeExecutionListResponse,
@ -155,7 +169,11 @@ class SnippetDraftWorkflowApi(Resource):
@console_ns.doc("sync_snippet_draft_workflow")
@console_ns.expect(console_ns.models.get(SnippetDraftSyncPayload.__name__))
@console_ns.response(200, "Draft workflow synced successfully")
@console_ns.response(
200,
"Draft workflow synced successfully",
console_ns.models[WorkflowRestoreResponse.__name__],
)
@console_ns.response(400, "Hash mismatch")
@setup_required
@login_required
@ -191,7 +209,11 @@ class SnippetDraftWorkflowApi(Resource):
@console_ns.route("/snippets/<uuid:snippet_id>/workflows/draft/config")
class SnippetDraftConfigApi(Resource):
@console_ns.doc("get_snippet_draft_config")
@console_ns.response(200, "Draft config retrieved successfully")
@console_ns.response(
200,
"Draft config retrieved successfully",
console_ns.models[SnippetDraftConfigResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -235,7 +257,7 @@ class SnippetPublishedWorkflowApi(Resource):
@console_ns.doc("publish_snippet_workflow")
@console_ns.expect(console_ns.models.get(PublishWorkflowPayload.__name__))
@console_ns.response(200, "Workflow published successfully")
@console_ns.response(200, "Workflow published successfully", console_ns.models[WorkflowPublishResponse.__name__])
@console_ns.response(400, "No draft workflow found")
@setup_required
@login_required
@ -269,7 +291,11 @@ class SnippetPublishedWorkflowApi(Resource):
@console_ns.route("/snippets/<uuid:snippet_id>/workflows/default-workflow-block-configs")
class SnippetDefaultBlockConfigsApi(Resource):
@console_ns.doc("get_snippet_default_block_configs")
@console_ns.response(200, "Default block configs retrieved successfully")
@console_ns.response(
200,
"Default block configs retrieved successfully",
console_ns.models[DefaultBlockConfigsResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -326,7 +352,7 @@ class SnippetDraftWorkflowRestoreApi(Resource):
@console_ns.doc("restore_snippet_workflow_to_draft")
@console_ns.doc(description="Restore a published snippet workflow version into the draft workflow")
@console_ns.doc(params={"snippet_id": "Snippet ID", "workflow_id": "Published workflow ID"})
@console_ns.response(200, "Workflow restored successfully")
@console_ns.response(200, "Workflow restored successfully", console_ns.models[WorkflowRestoreResponse.__name__])
@console_ns.response(400, "Source workflow must be published")
@console_ns.response(404, "Workflow not found")
@setup_required
@ -362,6 +388,7 @@ class SnippetDraftWorkflowRestoreApi(Resource):
@console_ns.route("/snippets/<uuid:snippet_id>/workflow-runs")
class SnippetWorkflowRunsApi(Resource):
@console_ns.doc("list_snippet_workflow_runs")
@console_ns.doc(params=query_params_from_model(WorkflowRunQuery))
@console_ns.response(
200,
"Workflow runs retrieved successfully",
@ -535,7 +562,11 @@ class SnippetDraftRunIterationNodeApi(Resource):
@console_ns.doc(description="Run draft workflow iteration node for snippet")
@console_ns.doc(params={"snippet_id": "Snippet ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models.get(SnippetIterationNodeRunPayload.__name__))
@console_ns.response(200, "Iteration node run started successfully (SSE stream)")
@console_ns.response(
200,
"Iteration node run started successfully (SSE stream)",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(404, "Snippet or draft workflow not found")
@setup_required
@login_required
@ -576,7 +607,11 @@ class SnippetDraftRunLoopNodeApi(Resource):
@console_ns.doc(description="Run draft workflow loop node for snippet")
@console_ns.doc(params={"snippet_id": "Snippet ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models.get(SnippetLoopNodeRunPayload.__name__))
@console_ns.response(200, "Loop node run started successfully (SSE stream)")
@console_ns.response(
200,
"Loop node run started successfully (SSE stream)",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(404, "Snippet or draft workflow not found")
@setup_required
@login_required
@ -615,7 +650,11 @@ class SnippetDraftRunLoopNodeApi(Resource):
class SnippetDraftWorkflowRunApi(Resource):
@console_ns.doc("run_snippet_draft_workflow")
@console_ns.expect(console_ns.models.get(SnippetDraftRunPayload.__name__))
@console_ns.response(200, "Draft workflow run started successfully (SSE stream)")
@console_ns.response(
200,
"Draft workflow run started successfully (SSE stream)",
console_ns.models[GeneratedAppResponse.__name__],
)
@console_ns.response(404, "Snippet or draft workflow not found")
@setup_required
@login_required
@ -654,7 +693,7 @@ class SnippetDraftWorkflowRunApi(Resource):
@console_ns.route("/snippets/<uuid:snippet_id>/workflow-runs/tasks/<string:task_id>/stop")
class SnippetWorkflowTaskStopApi(Resource):
@console_ns.doc("stop_snippet_workflow_task")
@console_ns.response(200, "Task stopped successfully")
@console_ns.response(200, "Task stopped successfully", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(404, "Snippet not found")
@setup_required
@login_required

View File

@ -23,6 +23,7 @@ from controllers.common.schema import query_params_from_model
from controllers.console import console_ns
from controllers.console.app.error import DraftWorkflowNotExist
from controllers.console.app.workflow_draft_variable import (
EnvironmentVariableListResponse,
WorkflowDraftVariableListQuery,
WorkflowDraftVariableUpdatePayload,
ensure_variable_access,
@ -306,7 +307,11 @@ class SnippetSystemVariableCollectionApi(Resource):
class SnippetEnvironmentVariableCollectionApi(Resource):
@console_ns.doc("get_snippet_environment_variables")
@console_ns.doc(description="Get environment variables from snippet draft workflow graph")
@console_ns.response(200, "Environment variables retrieved successfully")
@console_ns.response(
200,
"Environment variables retrieved successfully",
console_ns.models[EnvironmentVariableListResponse.__name__],
)
@console_ns.response(404, "Draft workflow not found")
@_snippet_draft_var_prerequisite
def get(self, _current_user: Account, snippet: CustomizedSnippet) -> dict[str, list[dict[str, Any]]]:

View File

@ -1,7 +1,10 @@
import logging
from typing import Any
from flask_restx import Resource
from pydantic import RootModel
from controllers.common.schema import register_response_schema_models
from controllers.console.wraps import (
account_initialization_required,
setup_required,
@ -14,8 +17,16 @@ from . import console_ns
logger = logging.getLogger(__name__)
class SchemaDefinitionsResponse(RootModel[Any]):
root: Any
register_response_schema_models(console_ns, SchemaDefinitionsResponse)
@console_ns.route("/spec/schema-definitions")
class SpecSchemaDefinitionsApi(Resource):
@console_ns.response(200, "Success", console_ns.models[SchemaDefinitionsResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import Forbidden
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
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 (
account_initialization_required,
@ -95,12 +95,7 @@ class TagListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.doc(
params={
"type": 'Tag type filter. Can be "knowledge", "app", or "snippet".',
"keyword": "Search keyword for tag name.",
}
)
@console_ns.doc(params=query_params_from_model(TagListQueryParam))
@console_ns.doc(responses={200: ("Success", [console_ns.models[TagResponse.__name__]])})
@with_current_tenant_id
def get(self, current_tenant_id: str):

View File

@ -6,7 +6,7 @@ from typing import Any, Literal
import pytz
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import BaseModel, Field, RootModel, field_validator, model_validator
from sqlalchemy import select
from werkzeug.exceptions import NotFound
@ -236,6 +236,10 @@ class EducationAutocompleteResponse(ResponseModel):
has_next: bool | None = None
class EducationActivateResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_schema_models(
console_ns,
AccountIntegrateResponse,
@ -248,6 +252,7 @@ register_response_schema_models(
console_ns,
AccountResponse,
AvatarUrlResponse,
EducationActivateResponse,
SimpleResultDataResponse,
SimpleResultResponse,
VerificationTokenResponse,
@ -556,6 +561,7 @@ class EducationVerifyApi(Resource):
@console_ns.route("/account/education")
class EducationApi(Resource):
@console_ns.expect(console_ns.models[EducationActivatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EducationActivateResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -1,5 +1,9 @@
from flask_restx import Resource, fields
from typing import Any
from flask_restx import Resource
from pydantic import RootModel
from controllers.common.schema import register_response_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
@ -13,6 +17,17 @@ from models import Account
from services.agent_service import AgentService
class AgentProviderListResponse(RootModel[list[dict[str, Any]]]):
root: list[dict[str, Any]]
class AgentProviderResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
register_response_schema_models(console_ns, AgentProviderListResponse, AgentProviderResponse)
@console_ns.route("/workspaces/current/agent-providers")
class AgentProviderListApi(Resource):
@console_ns.doc("list_agent_providers")
@ -20,7 +35,7 @@ class AgentProviderListApi(Resource):
@console_ns.response(
200,
"Success",
fields.List(fields.Raw(description="Agent provider information")),
console_ns.models[AgentProviderListResponse.__name__],
)
@setup_required
@login_required
@ -39,7 +54,7 @@ class AgentProviderApi(Resource):
@console_ns.response(
200,
"Success",
fields.Raw(description="Agent provider details"),
console_ns.models[AgentProviderResponse.__name__],
)
@setup_required
@login_required

View File

@ -61,11 +61,15 @@ class EndpointCreateResponse(BaseModel):
class EndpointListResponse(BaseModel):
endpoints: list[dict[str, Any]] = Field(description="Endpoint information")
endpoints: list[dict[str, Any]] = Field(
description="Endpoint information",
)
class PluginEndpointListResponse(BaseModel):
endpoints: list[dict[str, Any]] = Field(description="Endpoint information")
endpoints: list[dict[str, Any]] = Field(
description="Endpoint information",
)
class EndpointDeleteResponse(BaseModel):

View File

@ -2,7 +2,7 @@ from flask_restx import Resource
from pydantic import BaseModel
from werkzeug.exceptions import Forbidden
from controllers.common.schema import register_schema_models
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
@ -10,6 +10,7 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from fields.base import ResponseModel
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from libs.login import login_required
@ -23,7 +24,13 @@ class LoadBalancingCredentialPayload(BaseModel):
credentials: dict[str, object]
class LoadBalancingCredentialValidateResponse(ResponseModel):
result: str
error: str | None = None
register_schema_models(console_ns, LoadBalancingCredentialPayload)
register_response_schema_models(console_ns, LoadBalancingCredentialValidateResponse)
@console_ns.route(
@ -31,6 +38,11 @@ register_schema_models(console_ns, LoadBalancingCredentialPayload)
)
class LoadBalancingCredentialsValidateApi(Resource):
@console_ns.expect(console_ns.models[LoadBalancingCredentialPayload.__name__])
@console_ns.response(
200,
"Credential validation result",
console_ns.models[LoadBalancingCredentialValidateResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -75,6 +87,11 @@ class LoadBalancingCredentialsValidateApi(Resource):
)
class LoadBalancingConfigCredentialsValidateApi(Resource):
@console_ns.expect(console_ns.models[LoadBalancingCredentialPayload.__name__])
@console_ns.response(
200,
"Credential validation result",
console_ns.models[LoadBalancingCredentialValidateResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required

View File

@ -8,7 +8,7 @@ from sqlalchemy import func, select
import services
from configs import dify_config
from controllers.common.fields import SimpleResultDataResponse, VerificationTokenResponse
from controllers.common.fields import SimpleResultDataResponse, SimpleResultResponse, VerificationTokenResponse
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.auth.error import (
@ -29,6 +29,7 @@ from controllers.console.wraps import (
)
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from fields.base import ResponseModel
from fields.member_fields import AccountWithRole, AccountWithRoleList
from libs.helper import extract_remote_ip
from libs.login import login_required
@ -61,6 +62,24 @@ class OwnerTransferPayload(BaseModel):
token: str
class MemberInviteResultResponse(ResponseModel):
status: str
email: str
url: str | None = None
message: str | None = None
class MemberInviteResponse(ResponseModel):
result: str
invitation_results: list[MemberInviteResultResponse]
tenant_id: str
class MemberActionTenantResponse(ResponseModel):
result: str
tenant_id: str
register_enum_models(console_ns, TenantAccountRole)
register_schema_models(
console_ns,
@ -72,7 +91,14 @@ register_schema_models(
OwnerTransferCheckPayload,
OwnerTransferPayload,
)
register_response_schema_models(console_ns, SimpleResultDataResponse, VerificationTokenResponse)
register_response_schema_models(
console_ns,
SimpleResultDataResponse,
SimpleResultResponse,
VerificationTokenResponse,
MemberInviteResponse,
MemberActionTenantResponse,
)
def _is_role_enabled(role: TenantAccountRole | str, tenant_id: str) -> bool:
@ -152,6 +178,7 @@ class MemberInviteEmailApi(Resource):
"""Invite a new member by email."""
@console_ns.expect(console_ns.models[MemberInvitePayload.__name__])
@console_ns.response(201, "Success", console_ns.models[MemberInviteResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -221,6 +248,7 @@ class MemberInviteEmailApi(Resource):
class MemberCancelInviteApi(Resource):
"""Cancel an invitation by member id."""
@console_ns.response(200, "Success", console_ns.models[MemberActionTenantResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -254,6 +282,7 @@ class MemberUpdateRoleApi(Resource):
"""Update member role."""
@console_ns.expect(console_ns.models[MemberRoleUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -396,6 +425,7 @@ class OwnerTransferCheckApi(Resource):
@console_ns.route("/workspaces/current/members/<uuid:member_id>/owner-transfer")
class OwnerTransfer(Resource):
@console_ns.expect(console_ns.models[OwnerTransferPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -5,7 +5,7 @@ from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import BinaryFileResponse, 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 (
@ -15,6 +15,7 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from fields.base import ResponseModel
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
@ -22,6 +23,7 @@ from libs.helper import uuid_value
from libs.login import login_required
from models import Account
from services.billing_service import BillingService
from services.entities.model_provider_entities import ProviderResponse
from services.model_provider_service import ModelProviderService
@ -82,6 +84,23 @@ class ParserPreferredProviderType(BaseModel):
preferred_provider_type: Literal["system", "custom"]
class ModelProviderListResponse(ResponseModel):
data: list[ProviderResponse]
class ProviderCredentialResponse(ResponseModel):
credentials: dict[str, Any] | None = Field(default=None)
class ProviderCredentialValidateResponse(ResponseModel):
result: Literal["success", "error"]
error: str | None = None
class ModelProviderPaymentCheckoutUrlResponse(ResponseModel):
payment_link: str
register_schema_models(
console_ns,
ParserModelList,
@ -93,12 +112,21 @@ register_schema_models(
ParserCredentialValidate,
ParserPreferredProviderType,
)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(
console_ns,
BinaryFileResponse,
SimpleResultResponse,
ModelProviderListResponse,
ModelProviderPaymentCheckoutUrlResponse,
ProviderCredentialResponse,
ProviderCredentialValidateResponse,
)
@console_ns.route("/workspaces/current/model-providers")
class ModelProviderListApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserModelList))
@console_ns.response(200, "Success", console_ns.models[ModelProviderListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -116,6 +144,7 @@ class ModelProviderListApi(Resource):
@console_ns.route("/workspaces/current/model-providers/<path:provider>/credentials")
class ModelProviderCredentialApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserCredentialId))
@console_ns.response(200, "Success", console_ns.models[ProviderCredentialResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -133,6 +162,7 @@ class ModelProviderCredentialApi(Resource):
return {"credentials": credentials}
@console_ns.expect(console_ns.models[ParserCredentialCreate.__name__])
@console_ns.response(201, "Credential created successfully", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -157,6 +187,7 @@ class ModelProviderCredentialApi(Resource):
return {"result": "success"}, 201
@console_ns.expect(console_ns.models[ParserCredentialUpdate.__name__])
@console_ns.response(200, "Credential updated successfully", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -225,6 +256,11 @@ class ModelProviderCredentialSwitchApi(Resource):
@console_ns.route("/workspaces/current/model-providers/<path:provider>/credentials/validate")
class ModelProviderValidateApi(Resource):
@console_ns.expect(console_ns.models[ParserCredentialValidate.__name__])
@console_ns.response(
200,
"Credential validation result",
console_ns.models[ProviderCredentialValidateResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -262,6 +298,7 @@ class ModelProviderIconApi(Resource):
Get model provider icon
"""
@console_ns.response(200, "Success", console_ns.models[BinaryFileResponse.__name__])
def get(self, tenant_id: str, provider: str, icon_type: str, lang: str):
model_provider_service = ModelProviderService()
icon, mimetype = model_provider_service.get_model_provider_icon(
@ -298,6 +335,7 @@ class PreferredProviderTypeUpdateApi(Resource):
@console_ns.route("/workspaces/current/model-providers/<path:provider>/checkout-url")
class ModelProviderPaymentCheckoutUrlApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ModelProviderPaymentCheckoutUrlResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -20,12 +20,19 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from graphon.model_runtime.entities.model_entities import ModelType
from core.entities.provider_entities import CredentialConfiguration
from fields.base import ResponseModel
from graphon.model_runtime.entities.model_entities import ModelType, ParameterRule
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
from graphon.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import uuid_value
from libs.login import login_required
from models import Account
from services.entities.model_provider_entities import (
DefaultModelResponse,
ModelWithProviderEntityResponse,
ProviderWithModelsResponse,
)
from services.model_load_balancing_service import ModelLoadBalancingService
from services.model_provider_service import ModelProviderService
@ -52,7 +59,7 @@ class ParserDeleteModels(BaseModel):
class LoadBalancingPayload(BaseModel):
configs: list[dict[str, Any]] | None = None
configs: list[dict[str, Any]] | None = Field(default=None)
enabled: bool | None = None
@ -125,6 +132,40 @@ class ParserSwitch(BaseModel):
credential_id: str
class DefaultModelDataResponse(ResponseModel):
data: DefaultModelResponse | None = None
class ModelWithProviderListResponse(ResponseModel):
data: list[ModelWithProviderEntityResponse]
class ProviderWithModelsDataResponse(ResponseModel):
data: list[ProviderWithModelsResponse]
class ModelCredentialLoadBalancingResponse(ResponseModel):
enabled: bool
configs: list[dict[str, Any]] = Field(default_factory=list)
class ModelCredentialResponse(ResponseModel):
credentials: dict[str, Any] = Field(default_factory=dict)
current_credential_id: str | None = None
current_credential_name: str | None = None
load_balancing: ModelCredentialLoadBalancingResponse
available_credentials: list[CredentialConfiguration]
class ModelCredentialValidateResponse(ResponseModel):
result: str
error: str | None = None
class ModelParameterRulesResponse(ResponseModel):
data: list[ParameterRule]
register_schema_models(
console_ns,
ParserGetDefault,
@ -139,7 +180,16 @@ register_schema_models(
Inner,
ParserSwitch,
)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(
console_ns,
SimpleResultResponse,
DefaultModelDataResponse,
ModelWithProviderListResponse,
ProviderWithModelsDataResponse,
ModelCredentialResponse,
ModelCredentialValidateResponse,
ModelParameterRulesResponse,
)
register_enum_models(console_ns, ModelType)
@ -147,6 +197,7 @@ register_enum_models(console_ns, ModelType)
@console_ns.route("/workspaces/current/default-model")
class DefaultModelApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserGetDefault))
@console_ns.response(200, "Success", console_ns.models[DefaultModelDataResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -196,6 +247,7 @@ class DefaultModelApi(Resource):
@console_ns.route("/workspaces/current/model-providers/<path:provider>/models")
class ModelProviderModelApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ModelWithProviderListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -207,6 +259,7 @@ class ModelProviderModelApi(Resource):
return jsonable_encoder({"data": models})
@console_ns.expect(console_ns.models[ParserPostModels.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -273,6 +326,7 @@ class ModelProviderModelApi(Resource):
@console_ns.route("/workspaces/current/model-providers/<path:provider>/models/credentials")
class ModelProviderModelCredentialApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserGetCredentials))
@console_ns.response(200, "Success", console_ns.models[ModelCredentialResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -331,6 +385,7 @@ class ModelProviderModelCredentialApi(Resource):
)
@console_ns.expect(console_ns.models[ParserCreateCredential.__name__])
@console_ns.response(201, "Credential created successfully", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -362,6 +417,7 @@ class ModelProviderModelCredentialApi(Resource):
return {"result": "success"}, 201
@console_ns.expect(console_ns.models[ParserUpdateCredential.__name__])
@console_ns.response(200, "Credential updated successfully", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -486,6 +542,11 @@ register_schema_models(console_ns, ParserSwitch, ParserValidate)
@console_ns.route("/workspaces/current/model-providers/<path:provider>/models/credentials/validate")
class ModelProviderModelValidateApi(Resource):
@console_ns.expect(console_ns.models[ParserValidate.__name__])
@console_ns.response(
200,
"Credential validation result",
console_ns.models[ModelCredentialValidateResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -521,6 +582,7 @@ class ModelProviderModelValidateApi(Resource):
@console_ns.route("/workspaces/current/model-providers/<path:provider>/models/parameter-rules")
class ModelProviderModelParameterRuleApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserParameter))
@console_ns.response(200, "Success", console_ns.models[ModelParameterRulesResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -538,6 +600,7 @@ class ModelProviderModelParameterRuleApi(Resource):
@console_ns.route("/workspaces/current/models/model-types/<string:model_type>")
class ModelProviderAvailableModelApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ProviderWithModelsDataResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -4,12 +4,12 @@ from typing import Any, Literal
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.common.fields import SuccessResponse
from controllers.common.fields import BinaryFileResponse, SuccessResponse
from controllers.common.schema import (
query_params_from_model,
register_enum_models,
@ -157,6 +157,58 @@ class PluginDebuggingKeyResponse(ResponseModel):
port: int
class PluginDaemonOperationResponse(RootModel[Any]):
root: Any
class PluginListResponse(ResponseModel):
plugins: Any
total: int
class PluginVersionsResponse(ResponseModel):
versions: Any
class PluginInstallationsResponse(ResponseModel):
plugins: Any
class PluginManifestResponse(ResponseModel):
manifest: Any
class PluginTasksResponse(ResponseModel):
tasks: Any
class PluginTaskResponse(ResponseModel):
task: Any
class PluginPermissionResponse(ResponseModel):
install_permission: TenantPluginPermission.InstallPermission
debug_permission: TenantPluginPermission.DebugPermission
class PluginDynamicOptionsResponse(ResponseModel):
options: Any
class PluginOperationSuccessResponse(ResponseModel):
success: bool
message: str | None = None
class PluginPreferencesResponse(ResponseModel):
permission: PluginPermissionSettingsPayload
auto_upgrade: PluginAutoUpgradeSettingsPayload
class PluginReadmeResponse(ResponseModel):
readme: str
register_schema_models(
console_ns,
ParserList,
@ -180,7 +232,24 @@ register_schema_models(
ParserExcludePlugin,
ParserReadme,
)
register_response_schema_models(console_ns, PluginDebuggingKeyResponse, SuccessResponse)
register_response_schema_models(
console_ns,
BinaryFileResponse,
PluginDaemonOperationResponse,
PluginDebuggingKeyResponse,
PluginDynamicOptionsResponse,
PluginInstallationsResponse,
PluginListResponse,
PluginManifestResponse,
PluginOperationSuccessResponse,
PluginPermissionResponse,
PluginPreferencesResponse,
PluginReadmeResponse,
PluginTaskResponse,
PluginTasksResponse,
PluginVersionsResponse,
SuccessResponse,
)
register_enum_models(
console_ns,
@ -227,6 +296,7 @@ class PluginDebuggingKeyApi(Resource):
@console_ns.route("/workspaces/current/plugin/list")
class PluginListApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserList))
@console_ns.response(200, "Success", console_ns.models[PluginListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -245,6 +315,7 @@ class PluginListApi(Resource):
@console_ns.route("/workspaces/current/plugin/list/latest-versions")
class PluginListLatestVersionsApi(Resource):
@console_ns.expect(console_ns.models[ParserLatest.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginVersionsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -262,6 +333,7 @@ class PluginListLatestVersionsApi(Resource):
@console_ns.route("/workspaces/current/plugin/list/installations/ids")
class PluginListInstallationsFromIdsApi(Resource):
@console_ns.expect(console_ns.models[ParserLatest.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginInstallationsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -280,6 +352,7 @@ class PluginListInstallationsFromIdsApi(Resource):
@console_ns.route("/workspaces/current/plugin/icon")
class PluginIconApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserIcon))
@console_ns.response(200, "Success", console_ns.models[BinaryFileResponse.__name__])
@setup_required
def get(self):
args = ParserIcon.model_validate(request.args.to_dict(flat=True))
@ -296,6 +369,7 @@ class PluginIconApi(Resource):
@console_ns.route("/workspaces/current/plugin/asset")
class PluginAssetApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserAsset))
@console_ns.response(200, "Success", console_ns.models[BinaryFileResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -312,6 +386,7 @@ class PluginAssetApi(Resource):
@console_ns.route("/workspaces/current/plugin/upload/pkg")
class PluginUploadFromPkgApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -331,6 +406,7 @@ class PluginUploadFromPkgApi(Resource):
@console_ns.route("/workspaces/current/plugin/upload/github")
class PluginUploadFromGithubApi(Resource):
@console_ns.expect(console_ns.models[ParserGithubUpload.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -349,6 +425,7 @@ class PluginUploadFromGithubApi(Resource):
@console_ns.route("/workspaces/current/plugin/upload/bundle")
class PluginUploadFromBundleApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -368,6 +445,7 @@ class PluginUploadFromBundleApi(Resource):
@console_ns.route("/workspaces/current/plugin/install/pkg")
class PluginInstallFromPkgApi(Resource):
@console_ns.expect(console_ns.models[ParserPluginIdentifiers.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -387,6 +465,7 @@ class PluginInstallFromPkgApi(Resource):
@console_ns.route("/workspaces/current/plugin/install/github")
class PluginInstallFromGithubApi(Resource):
@console_ns.expect(console_ns.models[ParserGithubInstall.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -412,6 +491,7 @@ class PluginInstallFromGithubApi(Resource):
@console_ns.route("/workspaces/current/plugin/install/marketplace")
class PluginInstallFromMarketplaceApi(Resource):
@console_ns.expect(console_ns.models[ParserPluginIdentifiers.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -431,6 +511,7 @@ class PluginInstallFromMarketplaceApi(Resource):
@console_ns.route("/workspaces/current/plugin/marketplace/pkg")
class PluginFetchMarketplacePkgApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserPluginIdentifierQuery))
@console_ns.response(200, "Success", console_ns.models[PluginManifestResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -455,6 +536,7 @@ class PluginFetchMarketplacePkgApi(Resource):
@console_ns.route("/workspaces/current/plugin/fetch-manifest")
class PluginFetchManifestApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserPluginIdentifierQuery))
@console_ns.response(200, "Success", console_ns.models[PluginManifestResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -474,6 +556,7 @@ class PluginFetchManifestApi(Resource):
@console_ns.route("/workspaces/current/plugin/tasks")
class PluginFetchInstallTasksApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserTasks))
@console_ns.response(200, "Success", console_ns.models[PluginTasksResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -490,6 +573,7 @@ class PluginFetchInstallTasksApi(Resource):
@console_ns.route("/workspaces/current/plugin/tasks/<task_id>")
class PluginFetchInstallTaskApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginTaskResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -550,6 +634,7 @@ class PluginDeleteInstallTaskItemApi(Resource):
@console_ns.route("/workspaces/current/plugin/upgrade/marketplace")
class PluginUpgradeFromMarketplaceApi(Resource):
@console_ns.expect(console_ns.models[ParserMarketplaceUpgrade.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -571,6 +656,7 @@ class PluginUpgradeFromMarketplaceApi(Resource):
@console_ns.route("/workspaces/current/plugin/upgrade/github")
class PluginUpgradeFromGithubApi(Resource):
@console_ns.expect(console_ns.models[ParserGithubUpgrade.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDaemonOperationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -636,6 +722,7 @@ class PluginChangePermissionApi(Resource):
@console_ns.route("/workspaces/current/plugin/permission/fetch")
class PluginFetchPermissionApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginPermissionResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -661,6 +748,7 @@ class PluginFetchPermissionApi(Resource):
@console_ns.route("/workspaces/current/plugin/parameters/dynamic-options")
class PluginFetchDynamicSelectOptionsApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserDynamicOptions))
@console_ns.response(200, "Success", console_ns.models[PluginDynamicOptionsResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -690,6 +778,7 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
@console_ns.route("/workspaces/current/plugin/parameters/dynamic-options-with-credentials")
class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
@console_ns.expect(console_ns.models[ParserDynamicOptionsWithCredentials.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginDynamicOptionsResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -720,6 +809,7 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
@console_ns.route("/workspaces/current/plugin/preferences/change")
class PluginChangePreferencesApi(Resource):
@console_ns.expect(console_ns.models[ParserPreferencesChange.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginOperationSuccessResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -770,6 +860,7 @@ class PluginChangePreferencesApi(Resource):
@console_ns.route("/workspaces/current/plugin/preferences/fetch")
class PluginFetchPreferencesApi(Resource):
@console_ns.response(200, "Success", console_ns.models[PluginPreferencesResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -809,6 +900,7 @@ class PluginFetchPreferencesApi(Resource):
@console_ns.route("/workspaces/current/plugin/preferences/autoupgrade/exclude")
class PluginAutoUpgradeExcludePluginApi(Resource):
@console_ns.expect(console_ns.models[ParserExcludePlugin.__name__])
@console_ns.response(200, "Success", console_ns.models[PluginOperationSuccessResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -823,6 +915,7 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
@console_ns.route("/workspaces/current/plugin/readme")
class PluginReadmeApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserReadme))
@console_ns.response(200, "Success", console_ns.models[PluginReadmeResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -1,14 +1,17 @@
import logging
import re
from typing import Any
from urllib.parse import quote
from flask import Response, request
from flask_restx import Resource, marshal
from pydantic import RootModel
from sqlalchemy.orm import Session, sessionmaker
from werkzeug.datastructures import MultiDict
from werkzeug.exceptions import NotFound
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.common.fields import TextFileResponse
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.snippets.payloads import (
CreateSnippetPayload,
@ -25,6 +28,7 @@ from controllers.console.wraps import (
with_current_user,
)
from extensions.ext_database import db
from fields.base import ResponseModel
from fields.snippet_fields import snippet_fields, snippet_list_fields, snippet_pagination_fields
from libs.login import login_required
from models import Account
@ -38,6 +42,19 @@ _TAG_IDS_BRACKET_PATTERN = re.compile(r"^tag_ids\[(\d+)\]$")
_CREATOR_IDS_BRACKET_PATTERN = re.compile(r"^creator_ids\[(\d+)\]$")
class SnippetImportResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class SnippetDependencyCheckResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class SnippetUseCountResponse(ResponseModel):
result: str
use_count: int
def _snippet_service() -> SnippetService:
return SnippetService(sessionmaker(bind=db.engine, expire_on_commit=False))
@ -79,6 +96,13 @@ register_schema_models(
SnippetImportPayload,
IncludeSecretQuery,
)
register_response_schema_models(
console_ns,
TextFileResponse,
SnippetImportResponse,
SnippetDependencyCheckResponse,
SnippetUseCountResponse,
)
# Create namespace models for marshaling
snippet_model = console_ns.model("Snippet", snippet_fields)
@ -260,7 +284,8 @@ class CustomizedSnippetExportApi(Resource):
@console_ns.doc("export_customized_snippet")
@console_ns.doc(description="Export snippet configuration as DSL")
@console_ns.doc(params={"snippet_id": "Snippet ID to export"})
@console_ns.response(200, "Snippet exported successfully")
@console_ns.doc(params=query_params_from_model(IncludeSecretQuery))
@console_ns.response(200, "Snippet exported successfully", console_ns.models[TextFileResponse.__name__])
@console_ns.response(404, "Snippet not found")
@setup_required
@login_required
@ -304,8 +329,8 @@ class CustomizedSnippetImportApi(Resource):
@console_ns.doc("import_customized_snippet")
@console_ns.doc(description="Import snippet from DSL")
@console_ns.expect(console_ns.models.get(SnippetImportPayload.__name__))
@console_ns.response(200, "Snippet imported successfully")
@console_ns.response(202, "Import pending confirmation")
@console_ns.response(200, "Snippet imported successfully", console_ns.models[SnippetImportResponse.__name__])
@console_ns.response(202, "Import pending confirmation", console_ns.models[SnippetImportResponse.__name__])
@console_ns.response(400, "Import failed")
@setup_required
@login_required
@ -343,7 +368,7 @@ class CustomizedSnippetImportConfirmApi(Resource):
@console_ns.doc("confirm_snippet_import")
@console_ns.doc(description="Confirm a pending snippet import")
@console_ns.doc(params={"import_id": "Import ID to confirm"})
@console_ns.response(200, "Import confirmed successfully")
@console_ns.response(200, "Import confirmed successfully", console_ns.models[SnippetImportResponse.__name__])
@console_ns.response(400, "Import failed")
@setup_required
@login_required
@ -367,7 +392,11 @@ class CustomizedSnippetCheckDependenciesApi(Resource):
@console_ns.doc("check_snippet_dependencies")
@console_ns.doc(description="Check dependencies for a snippet")
@console_ns.doc(params={"snippet_id": "Snippet ID"})
@console_ns.response(200, "Dependencies checked successfully")
@console_ns.response(
200,
"Dependencies checked successfully",
console_ns.models[SnippetDependencyCheckResponse.__name__],
)
@console_ns.response(404, "Snippet not found")
@setup_required
@login_required
@ -397,7 +426,7 @@ class CustomizedSnippetUseCountIncrementApi(Resource):
@console_ns.doc("increment_snippet_use_count")
@console_ns.doc(description="Increment snippet use count by 1")
@console_ns.doc(params={"snippet_id": "Snippet ID"})
@console_ns.response(200, "Use count incremented successfully")
@console_ns.response(200, "Use count incremented successfully", console_ns.models[SnippetUseCountResponse.__name__])
@console_ns.response(404, "Snippet not found")
@setup_required
@login_required

View File

@ -5,13 +5,13 @@ from urllib.parse import urlparse
from flask import make_response, redirect, request, send_file
from flask_restx import Resource
from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator
from pydantic import BaseModel, Field, HttpUrl, RootModel, field_validator, model_validator
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.fields import BinaryFileResponse, RedirectResponse, 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 (
account_initialization_required,
@ -26,7 +26,7 @@ from core.entities.mcp_provider import IdentityMode, MCPAuthentication, MCPConfi
from core.mcp.auth.auth_flow import auth, handle_callback
from core.mcp.error import MCPAuthError, MCPError, MCPRefreshTokenError
from core.mcp.mcp_client import MCPClient
from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.entities.plugin_daemon import CredentialType, PluginOAuthAuthorizationUrlResponse
from core.plugin.impl.oauth import OAuthHandler
from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration
from extensions.ext_database import db
@ -77,7 +77,7 @@ class BuiltinToolAddPayload(BaseModel):
class BuiltinToolUpdatePayload(BaseModel):
credential_id: str
credentials: dict[str, Any] | None = None
credentials: dict[str, Any] | None = Field(default=None)
name: str | None = Field(default=None, max_length=30)
@ -108,6 +108,13 @@ class ProviderQuery(BaseModel):
provider: str
class BuiltinCredentialListQuery(BaseModel):
include_credential_ids: list[str] = Field(
default_factory=list,
description="Credential IDs to include even if visibility would hide them",
)
class ApiToolProviderDeletePayload(BaseModel):
provider: str
@ -199,7 +206,7 @@ class BuiltinProviderDefaultCredentialPayload(BaseModel):
class ToolOAuthCustomClientPayload(BaseModel):
client_params: dict[str, Any] | None = None
client_params: dict[str, Any] | None = Field(default=None)
enable_oauth_custom_client: bool | None = True
@ -261,8 +268,27 @@ class MCPCallbackQuery(BaseModel):
state: str
class ToolOAuthCustomClientResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class ToolOAuthClientSchemaResponse(RootModel[list[dict[str, Any]]]):
root: list[dict[str, Any]]
class ToolProviderOpaqueResponse(RootModel[Any]):
root: Any
register_schema_models(
console_ns,
ToolProviderListQuery,
UrlQuery,
ProviderQuery,
BuiltinCredentialListQuery,
WorkflowToolGetQuery,
WorkflowToolListQuery,
MCPCallbackQuery,
BuiltinToolCredentialDeletePayload,
BuiltinToolAddPayload,
BuiltinToolUpdatePayload,
@ -281,11 +307,22 @@ register_schema_models(
MCPProviderDeletePayload,
MCPAuthPayload,
)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(
console_ns,
BinaryFileResponse,
PluginOAuthAuthorizationUrlResponse,
RedirectResponse,
SimpleResultResponse,
ToolOAuthClientSchemaResponse,
ToolOAuthCustomClientResponse,
ToolProviderOpaqueResponse,
)
@console_ns.route("/workspaces/current/tool-providers")
class ToolProviderListApi(Resource):
@console_ns.doc(params=query_params_from_model(ToolProviderListQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -300,6 +337,7 @@ class ToolProviderListApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/tools")
class ToolBuiltinProviderListToolsApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -315,6 +353,7 @@ class ToolBuiltinProviderListToolsApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/info")
class ToolBuiltinProviderInfoApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -326,6 +365,7 @@ class ToolBuiltinProviderInfoApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/delete")
class ToolBuiltinProviderDeleteApi(Resource):
@console_ns.expect(console_ns.models[BuiltinToolCredentialDeletePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -344,6 +384,7 @@ class ToolBuiltinProviderDeleteApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/add")
class ToolBuiltinProviderAddApi(Resource):
@console_ns.expect(console_ns.models[BuiltinToolAddPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -366,6 +407,7 @@ class ToolBuiltinProviderAddApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/update")
class ToolBuiltinProviderUpdateApi(Resource):
@console_ns.expect(console_ns.models[BuiltinToolUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -388,6 +430,8 @@ class ToolBuiltinProviderUpdateApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/credentials")
class ToolBuiltinProviderGetCredentialsApi(Resource):
@console_ns.doc(params=query_params_from_model(BuiltinCredentialListQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -412,6 +456,7 @@ class ToolBuiltinProviderGetCredentialsApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/icon")
class ToolBuiltinProviderIconApi(Resource):
@console_ns.response(200, "Success", console_ns.models[BinaryFileResponse.__name__])
@setup_required
def get(self, provider: str):
icon_bytes, mimetype = BuiltinToolManageService.get_builtin_tool_provider_icon(provider)
@ -422,6 +467,7 @@ class ToolBuiltinProviderIconApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/add")
class ToolApiProviderAddApi(Resource):
@console_ns.expect(console_ns.models[ApiToolProviderAddPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -447,6 +493,8 @@ class ToolApiProviderAddApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/remote")
class ToolApiProviderGetRemoteSchemaApi(Resource):
@console_ns.doc(params=query_params_from_model(UrlQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -465,6 +513,8 @@ class ToolApiProviderGetRemoteSchemaApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/tools")
class ToolApiProviderListToolsApi(Resource):
@console_ns.doc(params=query_params_from_model(ProviderQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -486,6 +536,7 @@ class ToolApiProviderListToolsApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/update")
class ToolApiProviderUpdateApi(Resource):
@console_ns.expect(console_ns.models[ApiToolProviderUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -513,6 +564,7 @@ class ToolApiProviderUpdateApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/delete")
class ToolApiProviderDeleteApi(Resource):
@console_ns.expect(console_ns.models[ApiToolProviderDeletePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -531,6 +583,8 @@ class ToolApiProviderDeleteApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/get")
class ToolApiProviderGetApi(Resource):
@console_ns.doc(params=query_params_from_model(ProviderQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -549,6 +603,7 @@ class ToolApiProviderGetApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/credential/schema/<path:credential_type>")
class ToolBuiltinProviderCredentialsSchemaApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -564,6 +619,7 @@ class ToolBuiltinProviderCredentialsSchemaApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/schema")
class ToolApiProviderSchemaApi(Resource):
@console_ns.expect(console_ns.models[ApiToolSchemaPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -578,6 +634,7 @@ class ToolApiProviderSchemaApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/api/test/pre")
class ToolApiProviderPreviousTestApi(Resource):
@console_ns.expect(console_ns.models[ApiToolTestPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -598,6 +655,7 @@ class ToolApiProviderPreviousTestApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/workflow/create")
class ToolWorkflowProviderCreateApi(Resource):
@console_ns.expect(console_ns.models[WorkflowToolCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -624,6 +682,7 @@ class ToolWorkflowProviderCreateApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/workflow/update")
class ToolWorkflowProviderUpdateApi(Resource):
@console_ns.expect(console_ns.models[WorkflowToolUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -650,6 +709,7 @@ class ToolWorkflowProviderUpdateApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/workflow/delete")
class ToolWorkflowProviderDeleteApi(Resource):
@console_ns.expect(console_ns.models[WorkflowToolDeletePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -668,6 +728,8 @@ class ToolWorkflowProviderDeleteApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/workflow/get")
class ToolWorkflowProviderGetApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkflowToolGetQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -697,6 +759,8 @@ class ToolWorkflowProviderGetApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/workflow/tools")
class ToolWorkflowProviderListToolApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkflowToolListQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -717,6 +781,7 @@ class ToolWorkflowProviderListToolApi(Resource):
@console_ns.route("/workspaces/current/tools/builtin")
class ToolBuiltinListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -736,6 +801,7 @@ class ToolBuiltinListApi(Resource):
@console_ns.route("/workspaces/current/tools/api")
class ToolApiListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -753,6 +819,7 @@ class ToolApiListApi(Resource):
@console_ns.route("/workspaces/current/tools/workflow")
class ToolWorkflowListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -772,6 +839,7 @@ class ToolWorkflowListApi(Resource):
@console_ns.route("/workspaces/current/tool-labels")
class ToolLabelsApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -782,6 +850,11 @@ class ToolLabelsApi(Resource):
@console_ns.route("/oauth/plugin/<path:provider>/tool/authorization-url")
class ToolPluginOAuthApi(Resource):
@console_ns.response(
200,
"Authorization URL retrieved successfully",
console_ns.models[PluginOAuthAuthorizationUrlResponse.__name__],
)
@setup_required
@login_required
@is_admin_or_owner_required
@ -823,6 +896,11 @@ class ToolPluginOAuthApi(Resource):
@console_ns.route("/oauth/plugin/<path:provider>/tool/callback")
class ToolOAuthCallback(Resource):
@console_ns.response(
302,
"Redirect to console OAuth callback page",
console_ns.models[RedirectResponse.__name__],
)
@setup_required
def get(self, provider: str):
context_id = request.cookies.get("context_id")
@ -877,6 +955,7 @@ class ToolOAuthCallback(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/default-credential")
class ToolBuiltinProviderSetDefaultApi(Resource):
@console_ns.expect(console_ns.models[BuiltinProviderDefaultCredentialPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -892,6 +971,7 @@ class ToolBuiltinProviderSetDefaultApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/oauth/custom-client")
class ToolOAuthCustomClient(Resource):
@console_ns.expect(console_ns.models[ToolOAuthCustomClientPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -912,6 +992,7 @@ class ToolOAuthCustomClient(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[ToolOAuthCustomClientResponse.__name__])
@with_current_tenant_id
def get(self, current_tenant_id: str, provider: str):
return jsonable_encoder(
@ -921,6 +1002,7 @@ class ToolOAuthCustomClient(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_tenant_id
def delete(self, current_tenant_id: str, provider: str):
return jsonable_encoder(
@ -930,6 +1012,7 @@ class ToolOAuthCustomClient(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/oauth/client-schema")
class ToolBuiltinProviderGetOauthClientSchemaApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolOAuthClientSchemaResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -944,6 +1027,8 @@ class ToolBuiltinProviderGetOauthClientSchemaApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/builtin/<path:provider>/credential/info")
class ToolBuiltinProviderGetCredentialInfoApi(Resource):
@console_ns.doc(params=query_params_from_model(BuiltinCredentialListQuery))
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -967,6 +1052,7 @@ class ToolBuiltinProviderGetCredentialInfoApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/mcp")
class ToolProviderMCPApi(Resource):
@console_ns.expect(console_ns.models[MCPProviderCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1021,6 +1107,7 @@ class ToolProviderMCPApi(Resource):
return jsonable_encoder(result)
@console_ns.expect(console_ns.models[MCPProviderUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1091,6 +1178,7 @@ class ToolProviderMCPApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/mcp/auth")
class ToolMCPAuthApi(Resource):
@console_ns.expect(console_ns.models[MCPAuthPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1164,6 +1252,7 @@ class ToolMCPAuthApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/mcp/tools/<path:provider_id>")
class ToolMCPDetailApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1177,6 +1266,7 @@ class ToolMCPDetailApi(Resource):
@console_ns.route("/workspaces/current/tools/mcp")
class ToolMCPListAllApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1192,6 +1282,7 @@ class ToolMCPListAllApi(Resource):
@console_ns.route("/workspaces/current/tool-provider/mcp/update/<path:provider_id>")
class ToolMCPUpdateApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ToolProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -1208,6 +1299,12 @@ class ToolMCPUpdateApi(Resource):
@console_ns.route("/mcp/oauth/callback")
class ToolMCPCallbackApi(Resource):
@console_ns.doc(params=query_params_from_model(MCPCallbackQuery))
@console_ns.response(
302,
"Redirect to console OAuth callback page",
console_ns.models[RedirectResponse.__name__],
)
def get(self):
raw_args = request.args.to_dict()
query = MCPCallbackQuery.model_validate(raw_args)

View File

@ -3,13 +3,13 @@ from typing import Any
from flask import make_response, redirect, request
from flask_restx import Resource
from pydantic import BaseModel, model_validator
from pydantic import BaseModel, Field, RootModel, model_validator
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden
from configs import dify_config
from controllers.common.errors import NotFoundError
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import BinaryFileResponse, RedirectResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.impl.oauth import OAuthHandler
@ -48,9 +48,9 @@ class TriggerSubscriptionBuilderVerifyPayload(BaseModel):
class TriggerSubscriptionBuilderUpdatePayload(BaseModel):
name: str | None = None
parameters: dict[str, Any] | None = None
properties: dict[str, Any] | None = None
credentials: dict[str, Any] | None = None
parameters: dict[str, Any] | None = Field(default=None)
properties: dict[str, Any] | None = Field(default=None)
credentials: dict[str, Any] | None = Field(default=None)
@model_validator(mode="after")
def check_at_least_one_field(self):
@ -60,10 +60,30 @@ class TriggerSubscriptionBuilderUpdatePayload(BaseModel):
class TriggerOAuthClientPayload(BaseModel):
client_params: dict[str, Any] | None = None
client_params: dict[str, Any] | None = Field(default=None)
enabled: bool | None = None
class TriggerOAuthAuthorizeResponse(BaseModel):
authorization_url: str
subscription_builder_id: str
subscription_builder: Any
class TriggerOAuthClientResponse(BaseModel):
configured: bool
system_configured: bool
custom_configured: bool
oauth_client_schema: Any
custom_enabled: bool
redirect_uri: str
params: dict[str, Any]
class TriggerProviderOpaqueResponse(RootModel[Any]):
root: Any
register_schema_models(
console_ns,
TriggerSubscriptionBuilderCreatePayload,
@ -71,11 +91,20 @@ register_schema_models(
TriggerSubscriptionBuilderUpdatePayload,
TriggerOAuthClientPayload,
)
register_response_schema_models(console_ns, SimpleResultResponse)
register_response_schema_models(
console_ns,
BinaryFileResponse,
RedirectResponse,
SimpleResultResponse,
TriggerOAuthAuthorizeResponse,
TriggerOAuthClientResponse,
TriggerProviderOpaqueResponse,
)
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/icon")
class TriggerProviderIconApi(Resource):
@console_ns.response(200, "Success", console_ns.models[BinaryFileResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -86,6 +115,7 @@ class TriggerProviderIconApi(Resource):
@console_ns.route("/workspaces/current/triggers")
class TriggerProviderListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -97,6 +127,7 @@ class TriggerProviderListApi(Resource):
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/info")
class TriggerProviderInfoApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -108,6 +139,7 @@ class TriggerProviderInfoApi(Resource):
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/subscriptions/list")
class TriggerSubscriptionListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -136,6 +168,7 @@ class TriggerSubscriptionListApi(Resource):
)
class TriggerSubscriptionBuilderCreateApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -164,6 +197,7 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderGetApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -180,6 +214,7 @@ class TriggerSubscriptionBuilderGetApi(Resource):
)
class TriggerSubscriptionBuilderVerifyApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -211,6 +246,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
)
class TriggerSubscriptionBuilderUpdateApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -242,6 +278,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/logs/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderLogsApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -261,6 +298,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource):
)
class TriggerSubscriptionBuilderBuildApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -294,6 +332,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
)
class TriggerSubscriptionUpdateApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required
@ -382,6 +421,11 @@ class TriggerSubscriptionDeleteApi(Resource):
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/subscriptions/oauth/authorize")
class TriggerOAuthAuthorizeApi(Resource):
@console_ns.response(
200,
"Authorization URL retrieved successfully",
console_ns.models[TriggerOAuthAuthorizeResponse.__name__],
)
@setup_required
@login_required
@account_initialization_required
@ -463,6 +507,11 @@ class TriggerOAuthAuthorizeApi(Resource):
@console_ns.route("/oauth/plugin/<path:provider>/trigger/callback")
class TriggerOAuthCallbackApi(Resource):
@console_ns.response(
302,
"Redirect to console OAuth callback page",
console_ns.models[RedirectResponse.__name__],
)
@setup_required
def get(self, provider: str):
"""Handle OAuth callback for trigger provider"""
@ -528,6 +577,7 @@ class TriggerOAuthCallbackApi(Resource):
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/oauth/client")
class TriggerOAuthClientManageApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TriggerOAuthClientResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -572,6 +622,7 @@ class TriggerOAuthClientManageApi(Resource):
raise
@console_ns.expect(console_ns.models[TriggerOAuthClientPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@is_admin_or_owner_required
@ -600,6 +651,7 @@ class TriggerOAuthClientManageApi(Resource):
@login_required
@is_admin_or_owner_required
@account_initialization_required
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@with_current_tenant_id
def delete(self, tenant_id: str, provider: str):
"""Remove custom OAuth client configuration"""
@ -622,6 +674,7 @@ class TriggerOAuthClientManageApi(Resource):
)
class TriggerSubscriptionVerifyApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[TriggerProviderOpaqueResponse.__name__])
@setup_required
@login_required
@edit_permission_required

View File

@ -96,6 +96,76 @@ class TenantInfoResponse(ResponseModel):
return to_timestamp(value)
class TenantListItemResponse(ResponseModel):
id: str
name: str | None = None
plan: str | None = None
status: str | None = None
created_at: int | None = None
current: bool
@field_validator("plan", "status", mode="before")
@classmethod
def _normalize_enum_like(cls, value):
if value is None:
return None
if isinstance(value, str):
return value
return str(getattr(value, "value", value))
@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | int | None):
return to_timestamp(value)
class TenantListResponse(ResponseModel):
workspaces: list[TenantListItemResponse]
class WorkspaceListItemResponse(ResponseModel):
id: str
name: str | None = None
status: str | None = None
created_at: int | None = None
@field_validator("status", mode="before")
@classmethod
def _normalize_status(cls, value):
if value is None:
return None
if isinstance(value, str):
return value
return str(getattr(value, "value", value))
@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | int | None):
return to_timestamp(value)
class WorkspaceListResponse(ResponseModel):
data: list[WorkspaceListItemResponse]
has_more: bool
limit: int
page: int
total: int
class SwitchWorkspaceResponse(ResponseModel):
result: str
new_tenant: TenantInfoResponse
class WorkspaceMutationResponse(ResponseModel):
result: str
tenant: TenantInfoResponse
class WorkspaceLogoUploadResponse(ResponseModel):
id: str
class WorkspacePermissionResponse(ResponseModel):
workspace_id: str
allow_member_invite: bool
@ -112,6 +182,11 @@ register_schema_models(
register_response_schema_models(
console_ns,
TenantInfoResponse,
TenantListResponse,
WorkspaceListResponse,
SwitchWorkspaceResponse,
WorkspaceMutationResponse,
WorkspaceLogoUploadResponse,
WorkspaceCustomConfigResponse,
WorkspacePermissionResponse,
)
@ -152,6 +227,7 @@ workspace_fields = {"id": fields.String, "name": fields.String, "status": fields
@console_ns.route("/workspaces")
class TenantListApi(Resource):
@console_ns.response(200, "Success", console_ns.models[TenantListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -202,6 +278,7 @@ class TenantListApi(Resource):
@console_ns.route("/all-workspaces")
class WorkspaceListApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkspaceListQuery))
@console_ns.response(200, "Success", console_ns.models[WorkspaceListResponse.__name__])
@setup_required
@admin_required
def get(self):
@ -256,6 +333,7 @@ class TenantApi(Resource):
@console_ns.route("/workspaces/switch")
class SwitchWorkspaceApi(Resource):
@console_ns.expect(console_ns.models[SwitchWorkspacePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SwitchWorkspaceResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -280,6 +358,7 @@ class SwitchWorkspaceApi(Resource):
@console_ns.route("/workspaces/custom-config")
class CustomConfigWorkspaceApi(Resource):
@console_ns.expect(console_ns.models[WorkspaceCustomConfigPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -307,6 +386,7 @@ class CustomConfigWorkspaceApi(Resource):
@console_ns.route("/workspaces/custom-config/webapp-logo/upload")
class WebappLogoWorkspaceApi(Resource):
@console_ns.response(201, "Logo uploaded", console_ns.models[WorkspaceLogoUploadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -348,6 +428,7 @@ class WebappLogoWorkspaceApi(Resource):
@console_ns.route("/workspaces/info")
class WorkspaceInfoApi(Resource):
@console_ns.expect(console_ns.models[WorkspaceInfoPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[WorkspaceMutationResponse.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -20,6 +20,7 @@ openapi_ns = Namespace("openapi", description="User-scoped operations", path="/"
# Register response/query models BEFORE importing controller modules so that
# @openapi_ns.response / @openapi_ns.expect decorators can resolve model names.
from controllers.common.fields import EventStreamResponse
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
from controllers.openapi._models import (
AccountPayload,
@ -42,8 +43,10 @@ from controllers.openapi._models import (
DeviceMutateRequest,
DeviceMutateResponse,
DevicePollRequest,
DeviceTokenResponse,
FormSubmitResponse,
HealthResponse,
HumanInputFormDefinitionResponse,
MemberActionResponse,
MemberInvitePayload,
MemberInviteResponse,
@ -92,6 +95,7 @@ register_schema_models(
register_response_schema_models(
openapi_ns,
ErrorBody,
EventStreamResponse,
TagItem,
UsageInfo,
MessageMetadata,
@ -120,7 +124,9 @@ register_response_schema_models(
MemberActionResponse,
TaskStopResponse,
FormSubmitResponse,
HumanInputFormDefinitionResponse,
DeviceCodeResponse,
DeviceTokenResponse,
DeviceLookupResponse,
DeviceMutateResponse,
FileResponse,

View File

@ -87,12 +87,8 @@ class AppDescribeInfo(AppInfoResponse):
class AppDescribeResponse(BaseModel):
info: AppDescribeInfo | None = None
# `parameters` (the app-config blob) and `input_schema` (a Draft 2020-12 JSON Schema derived
# per-app) are deliberately open JSON, not under-annotated. The `x-dify-opaque` marker tells the
# contract generator's readiness detector to treat them as intentional, so the route is not
# flagged "annotations incomplete". CLI/web consume them as opaque objects either way.
parameters: dict[str, Any] | None = Field(default=None, json_schema_extra={"x-dify-opaque": True})
input_schema: dict[str, Any] | None = Field(default=None, json_schema_extra={"x-dify-opaque": True})
parameters: dict[str, Any] | None = Field(default=None)
input_schema: dict[str, Any] | None = Field(default=None)
class ChatMessageResponse(BaseModel):
@ -150,6 +146,18 @@ class WorkspacePayload(BaseModel):
role: str
class DeviceTokenResponse(BaseModel):
token: str
expires_at: str
subject_type: Literal["account", "external_sso"]
account: AccountPayload | None = None
workspaces: list[WorkspacePayload] = []
default_workspace_id: str | None = None
token_id: str
subject_email: str | None = None
subject_issuer: str | None = None
class AccountResponse(BaseModel):
subject_type: str
subject_email: str | None = None
@ -292,7 +300,7 @@ class AppListQuery(BaseModel):
class AppRunRequest(BaseModel):
inputs: dict[str, Any]
query: str | None = None
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
conversation_id: UUIDStrOrEmpty | None = None
auto_generate_name: bool = True
workflow_id: str | None = None
@ -469,3 +477,11 @@ class FormSubmitResponse(BaseModel):
than an under-annotated open object."""
model_config = ConfigDict(extra="forbid")
class HumanInputFormDefinitionResponse(BaseModel):
form_content: str
inputs: list[dict[str, Any]] = Field(default_factory=list)
resolved_default_values: dict[str, str]
user_actions: list[dict[str, Any]] = Field(default_factory=list)
expiration_time: int | None = None

View File

@ -18,6 +18,7 @@ from werkzeug.exceptions import (
)
import services
from controllers.common.fields import EventStreamResponse
from controllers.openapi import openapi_ns
from controllers.openapi._audit import emit_app_run
from controllers.openapi._contract import accepts, returns
@ -136,7 +137,7 @@ _DISPATCH: dict[AppMode, Callable[[App, Any, AppRunRequest], Any]] = {
@openapi_ns.route("/apps/<string:app_id>/run")
class AppRunApi(Resource):
@auth_router.guard(scope=Scope.APPS_RUN)
@openapi_ns.response(200, "Run result (SSE stream)")
@openapi_ns.response(200, "Run result (SSE stream)", openapi_ns.models[EventStreamResponse.__name__])
@accepts(body=AppRunRequest)
def post(self, app_id: str, *, auth_data: AuthData, body: AppRunRequest):
app_model, caller, caller_kind = auth_data.require_app_context()

View File

@ -18,7 +18,7 @@ from controllers.common.human_input import HumanInputFormSubmitPayload, stringif
from controllers.common.schema import register_schema_models
from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns
from controllers.openapi._models import FormSubmitResponse
from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData
from core.workflow.human_input_policy import HumanInputSurface, is_recipient_type_allowed_for_surface
@ -57,7 +57,7 @@ def _ensure_form_is_allowed_for_openapi(form) -> None:
@openapi_ns.route("/apps/<string:app_id>/form/human_input/<string:form_token>")
class OpenApiWorkflowHumanInputFormApi(Resource):
@openapi_ns.response(200, "Form definition")
@openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__])
@auth_router.guard(scope=Scope.APPS_RUN)
def get(self, app_id: str, form_token: str, *, auth_data: AuthData):
app_model, caller, caller_kind = auth_data.require_app_context()

View File

@ -42,6 +42,7 @@ from controllers.openapi._models import (
DeviceMutateRequest,
DeviceMutateResponse,
DevicePollRequest,
DeviceTokenResponse,
WorkspacePayload,
)
from extensions.ext_database import db
@ -130,6 +131,7 @@ class OAuthDeviceTokenApi(Resource):
"""RFC 8628 poll."""
@openapi_ns.expect(openapi_ns.models[DevicePollRequest.__name__])
@openapi_ns.response(200, "Device token", openapi_ns.models[DeviceTokenResponse.__name__])
def post(self):
payload = _validate_json(DevicePollRequest)
device_code = payload.device_code

View File

@ -13,9 +13,12 @@ from collections.abc import Generator
from flask import Response, request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound, UnprocessableEntity
from controllers.common.fields import EventStreamResponse
from controllers.common.schema import query_params_from_model
from controllers.openapi import openapi_ns
from controllers.openapi.auth.composition import auth_router
from controllers.openapi.auth.data import AuthData
@ -34,9 +37,15 @@ from repositories.factory import DifyAPIRepositoryFactory
from services.workflow_event_snapshot_service import build_workflow_event_stream
class WorkflowEventsQuery(BaseModel):
include_state_snapshot: bool = Field(default=False, description="Whether to include workflow state snapshots")
continue_on_pause: bool = Field(default=False, description="Whether to keep the event stream open on pause")
@openapi_ns.route("/apps/<string:app_id>/tasks/<string:task_id>/events")
class OpenApiWorkflowEventsApi(Resource):
@openapi_ns.response(200, "SSE event stream")
@openapi_ns.doc(params=query_params_from_model(WorkflowEventsQuery))
@openapi_ns.response(200, "SSE event stream", openapi_ns.models[EventStreamResponse.__name__])
@auth_router.guard(scope=Scope.APPS_RUN)
def get(self, app_id: str, task_id: str, *, auth_data: AuthData):
app_model, caller, caller_kind = auth_data.require_app_context()

View File

@ -6,12 +6,13 @@ from flask_restx import Resource
from flask_restx.api import HTTPStatus
from pydantic import BaseModel, Field, TypeAdapter
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console.wraps import edit_permission_required
from controllers.service_api import service_api_ns
from controllers.service_api.wraps import validate_app_token
from extensions.ext_redis import redis_client
from fields.annotation_fields import Annotation, AnnotationList
from fields.base import ResponseModel
from models.model import App
from services.annotation_service import (
AppAnnotationService,
@ -38,6 +39,12 @@ class AnnotationListQuery(BaseModel):
keyword: str = Field(default="", description="Keyword to search annotations")
class AnnotationJobStatusResponse(ResponseModel):
job_id: str
job_status: str
error_msg: str | None = None
register_schema_models(
service_api_ns,
AnnotationCreatePayload,
@ -46,6 +53,7 @@ register_schema_models(
Annotation,
AnnotationList,
)
register_response_schema_models(service_api_ns, AnnotationJobStatusResponse)
@service_api_ns.route("/apps/annotation-reply/<string:action>")
@ -60,6 +68,11 @@ class AnnotationReplyActionApi(Resource):
401: "Unauthorized - invalid API token",
}
)
@service_api_ns.response(
200,
"Action completed successfully",
service_api_ns.models[AnnotationJobStatusResponse.__name__],
)
@validate_app_token
def post(self, app_model: App, action: Literal["enable", "disable"]):
"""Enable or disable annotation reply feature."""
@ -89,6 +102,11 @@ class AnnotationReplyActionStatusApi(Resource):
404: "Job not found",
}
)
@service_api_ns.response(
200,
"Job status retrieved successfully",
service_api_ns.models[AnnotationJobStatusResponse.__name__],
)
@validate_app_token
def get(self, app_model: App, job_id: UUID, action: str):
"""Get the status of an annotation reply action job."""

View File

@ -1,6 +1,7 @@
from typing import Any, cast
from flask_restx import Resource
from pydantic import Field
from controllers.common.fields import Parameters
from controllers.common.schema import register_response_schema_models
@ -21,7 +22,11 @@ class AppInfoResponse(ResponseModel):
author_name: str | None
register_response_schema_models(service_api_ns, AppInfoResponse)
class AppMetaResponse(ResponseModel):
tool_icons: dict[str, Any] = Field(default_factory=dict)
register_response_schema_models(service_api_ns, Parameters, AppMetaResponse, AppInfoResponse)
@service_api_ns.route("/parameters")
@ -37,6 +42,7 @@ class AppParameterApi(Resource):
404: "Application not found",
}
)
@service_api_ns.response(200, "Parameters retrieved successfully", service_api_ns.models[Parameters.__name__])
@validate_app_token
def get(self, app_model: App):
"""Retrieve app parameters.
@ -74,6 +80,7 @@ class AppMetaApi(Resource):
404: "Application not found",
}
)
@service_api_ns.response(200, "Metadata retrieved successfully", service_api_ns.models[AppMetaResponse.__name__])
@validate_app_token
def get(self, app_model: App):
"""Get app metadata.

View File

@ -6,7 +6,8 @@ from werkzeug.exceptions import InternalServerError
import services
from controllers.common.controller_schemas import TextToAudioPayload
from controllers.common.schema import register_schema_model
from controllers.common.fields import AudioBinaryResponse, AudioTranscriptResponse
from controllers.common.schema import register_response_schema_models, register_schema_model
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import (
AppUnavailableError,
@ -33,6 +34,8 @@ from services.errors.audio import (
logger = logging.getLogger(__name__)
register_response_schema_models(service_api_ns, AudioBinaryResponse, AudioTranscriptResponse)
@service_api_ns.route("/audio-to-text")
class AudioApi(Resource):
@ -48,6 +51,11 @@ class AudioApi(Resource):
500: "Internal server error",
}
)
@service_api_ns.response(
200,
"Audio successfully transcribed",
service_api_ns.models[AudioTranscriptResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM))
def post(self, app_model: App, end_user: EndUser):
"""Convert audio to text using speech-to-text.
@ -102,6 +110,11 @@ class TextApi(Resource):
500: "Internal server error",
}
)
@service_api_ns.response(
200,
"Text successfully converted to audio",
service_api_ns.models[AudioBinaryResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
def post(self, app_model: App, end_user: EndUser):
"""Convert text to audio using text-to-speech.

View File

@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import (
@ -53,7 +53,7 @@ def _resolve_agent_app_streaming(*, app_mode: AppMode, response_mode: str | None
class CompletionRequestPayload(BaseModel):
inputs: dict[str, Any]
query: str = Field(default="")
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
response_mode: Literal["blocking", "streaming"] | None = None
retriever_from: str = Field(default="dev")
trace_session_id: str | None = Field(default=None, description="Trace session ID for observability grouping")
@ -62,7 +62,7 @@ class CompletionRequestPayload(BaseModel):
class ChatRequestPayload(BaseModel):
inputs: dict[str, Any]
query: str
files: list[dict[str, Any]] | None = None
files: list[dict[str, Any]] | None = Field(default=None)
response_mode: Literal["blocking", "streaming"] | None = None
conversation_id: UUIDStrOrEmpty | None = Field(default=None, description="Conversation UUID")
retriever_from: str = Field(default="dev")
@ -87,7 +87,7 @@ class ChatRequestPayload(BaseModel):
register_schema_models(service_api_ns, CompletionRequestPayload, ChatRequestPayload)
register_response_schema_models(service_api_ns, SimpleResultResponse)
register_response_schema_models(service_api_ns, GeneratedAppResponse, SimpleResultResponse)
@service_api_ns.route("/completion-messages")
@ -104,6 +104,11 @@ class CompletionApi(Resource):
500: "Internal server error",
}
)
@service_api_ns.response(
200,
"Completion created successfully",
service_api_ns.models[GeneratedAppResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser):
"""Create a completion for the given prompt.
@ -205,6 +210,11 @@ class ChatApi(Resource):
500: "Internal server error",
}
)
@service_api_ns.response(
200,
"Message sent successfully",
service_api_ns.models[GeneratedAppResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser):
"""Send a message in a chat conversation.

View File

@ -10,7 +10,7 @@ from werkzeug.exceptions import BadRequest, NotFound
import services
from controllers.common.controller_schemas import ConversationRenamePayload
from controllers.common.schema import query_params_from_model, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import NotChatAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
@ -134,6 +134,13 @@ register_schema_models(
ConversationVariableResponse,
ConversationVariableInfiniteScrollPaginationResponse,
)
register_response_schema_models(
service_api_ns,
ConversationInfiniteScrollPagination,
SimpleConversation,
ConversationVariableResponse,
ConversationVariableInfiniteScrollPaginationResponse,
)
@service_api_ns.route("/conversations")
@ -148,6 +155,11 @@ class ConversationApi(Resource):
404: "Last conversation not found",
}
)
@service_api_ns.response(
200,
"Conversations retrieved successfully",
service_api_ns.models[ConversationInfiniteScrollPagination.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
def get(self, app_model: App, end_user: EndUser):
"""List all conversations for the current user.
@ -224,6 +236,11 @@ class ConversationRenameApi(Resource):
404: "Conversation not found",
}
)
@service_api_ns.response(
200,
"Conversation renamed successfully",
service_api_ns.models[SimpleConversation.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
def post(self, app_model: App, end_user: EndUser, c_id: UUID):
"""Rename a conversation or auto-generate a name."""

View File

@ -7,8 +7,9 @@ from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy import select
from controllers.common.fields import BinaryFileResponse
from controllers.common.file_response import enforce_download_for_html
from controllers.common.schema import query_params_from_model, register_schema_model
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_model
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import (
FileAccessDeniedError,
@ -27,6 +28,7 @@ class FilePreviewQuery(BaseModel):
register_schema_model(service_api_ns, FilePreviewQuery)
register_response_schema_model(service_api_ns, BinaryFileResponse)
@service_api_ns.route("/files/<uuid:file_id>/preview")
@ -50,6 +52,11 @@ class FilePreviewApi(Resource):
404: "File not found",
}
)
@service_api_ns.response(
200,
"File retrieved successfully",
service_api_ns.models[BinaryFileResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
def get(self, app_model: App, end_user: EndUser, file_id: UUID):
"""

View File

@ -8,17 +8,20 @@ paused human input forms in workflow/chatflow runs.
import json
import logging
from collections.abc import Sequence
from typing import Any
from flask import Response
from flask_restx import Resource
from pydantic import ConfigDict, Field
from werkzeug.exceptions import BadRequest, NotFound
from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values
from controllers.common.schema import register_schema_models
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from core.workflow.human_input_policy import HumanInputSurface, is_recipient_type_allowed_for_surface
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.nodes.human_input.entities import FormInputConfig
from libs.helper import to_timestamp
from models.model import App, EndUser
@ -27,7 +30,20 @@ from services.human_input_service import Form, FormNotFoundError, HumanInputServ
logger = logging.getLogger(__name__)
class HumanInputFormDefinitionResponse(ResponseModel):
form_content: str
inputs: list[dict[str, Any]] = Field(default_factory=list)
resolved_default_values: dict[str, str]
user_actions: list[dict[str, Any]] = Field(default_factory=list)
expiration_time: int | None = None
class HumanInputFormSubmitResponse(ResponseModel):
model_config = ConfigDict(extra="forbid")
register_schema_models(service_api_ns, HumanInputFormSubmitPayload)
register_response_schema_models(service_api_ns, HumanInputFormDefinitionResponse, HumanInputFormSubmitResponse)
def _jsonify_form_definition(form: Form, *, inputs: Sequence[FormInputConfig] = ()) -> Response:
@ -67,6 +83,11 @@ class WorkflowHumanInputFormApi(Resource):
412: "Form already submitted or expired",
}
)
@service_api_ns.response(
200,
"Form retrieved successfully",
service_api_ns.models[HumanInputFormDefinitionResponse.__name__],
)
@validate_app_token
def get(self, app_model: App, form_token: str):
service = HumanInputService(db.engine)
@ -93,6 +114,11 @@ class WorkflowHumanInputFormApi(Resource):
412: "Form already submitted or expired",
}
)
@service_api_ns.response(
200,
"Form submitted successfully",
service_api_ns.models[HumanInputFormSubmitResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser, form_token: str):
payload = HumanInputFormSubmitPayload.model_validate(service_api_ns.payload or {})

View File

@ -14,6 +14,7 @@ from controllers.service_api import service_api_ns
from controllers.service_api.app.error import NotChatAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from core.app.entities.app_invoke_entities import InvokeFrom
from fields.base import ResponseModel
from fields.conversation_fields import ResultResponse
from fields.message_fields import MessageInfiniteScrollPagination, MessageListItem
from models.enums import FeedbackRating
@ -33,8 +34,32 @@ class FeedbackListQuery(BaseModel):
limit: int = Field(default=20, ge=1, le=101, description="Number of feedbacks per page")
class AppFeedbackResponse(ResponseModel):
id: str
app_id: str
conversation_id: str
message_id: str
rating: str
content: str | None = None
from_source: str
from_end_user_id: str | None = None
from_account_id: str | None = None
created_at: str
updated_at: str
class AppFeedbackListResponse(ResponseModel):
data: list[AppFeedbackResponse]
register_schema_models(service_api_ns, MessageListQuery, MessageFeedbackPayload, FeedbackListQuery)
register_response_schema_models(service_api_ns, ResultResponse, SimpleResultStringListResponse)
register_response_schema_models(
service_api_ns,
ResultResponse,
SimpleResultStringListResponse,
MessageInfiniteScrollPagination,
AppFeedbackListResponse,
)
@service_api_ns.route("/messages")
@ -49,6 +74,11 @@ class MessageListApi(Resource):
404: "Conversation or first message not found",
}
)
@service_api_ns.response(
200,
"Messages retrieved successfully",
service_api_ns.models[MessageInfiniteScrollPagination.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
def get(self, app_model: App, end_user: EndUser):
"""List messages in a conversation.
@ -129,6 +159,11 @@ class AppGetFeedbacksApi(Resource):
401: "Unauthorized - invalid API token",
}
)
@service_api_ns.response(
200,
"Feedbacks retrieved successfully",
service_api_ns.models[AppFeedbackListResponse.__name__],
)
@validate_app_token
def get(self, app_model: App):
"""Get all feedbacks for the application.

View File

@ -11,7 +11,7 @@ from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from controllers.common.controller_schemas import WorkflowRunPayload as WorkflowRunPayloadBase
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import (
@ -69,7 +69,7 @@ class WorkflowLogQuery(BaseModel):
register_schema_models(service_api_ns, WorkflowRunPayload, WorkflowLogQuery)
register_response_schema_models(service_api_ns, SimpleResultResponse)
register_response_schema_models(service_api_ns, GeneratedAppResponse, SimpleResultResponse)
def _enum_value(value):
@ -97,7 +97,7 @@ class WorkflowRunResponse(ResponseModel):
id: str
workflow_id: str
status: str
inputs: dict | list | str | int | float | bool | None = None
inputs: dict | list | str | int | float | bool | None = Field(default=None)
outputs: dict = Field(default_factory=dict)
error: str | None = None
total_steps: int | None = None
@ -139,7 +139,7 @@ class WorkflowRunForLogResponse(ResponseModel):
class WorkflowAppLogPartialResponse(ResponseModel):
id: str
workflow_run: WorkflowRunForLogResponse | None = None
details: dict | list | str | int | float | bool | None = None
details: dict | list | str | int | float | bool | None = Field(default=None)
created_from: str | None = None
created_by_role: str | None = None
created_by_account: SimpleAccount | None = None
@ -165,7 +165,7 @@ class WorkflowAppLogPaginationResponse(ResponseModel):
data: list[WorkflowAppLogPartialResponse]
register_schema_models(
register_response_schema_models(
service_api_ns,
WorkflowRunResponse,
WorkflowRunForLogResponse,
@ -262,6 +262,11 @@ class WorkflowRunApi(Resource):
500: "Internal server error",
}
)
@service_api_ns.response(
200,
"Workflow executed successfully",
service_api_ns.models[GeneratedAppResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser):
"""Execute a workflow.
@ -322,6 +327,11 @@ class WorkflowRunByIdApi(Resource):
500: "Internal server error",
}
)
@service_api_ns.response(
200,
"Workflow executed successfully",
service_api_ns.models[GeneratedAppResponse.__name__],
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser, workflow_id: str):
"""Run specific workflow by ID.

View File

@ -7,9 +7,12 @@ from collections.abc import Generator
from flask import Response, request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from controllers.common.fields import EventStreamResponse
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import NotWorkflowAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
@ -27,26 +30,24 @@ from repositories.factory import DifyAPIRepositoryFactory
from services.workflow_event_snapshot_service import build_workflow_event_stream
class WorkflowEventsQuery(BaseModel):
user: str = Field(..., description="End user identifier")
include_state_snapshot: bool = Field(default=False, description="Replay from persisted state snapshot")
continue_on_pause: bool = Field(default=False, description="Keep the stream open across workflow_paused events")
register_schema_models(service_api_ns, WorkflowEventsQuery)
register_response_schema_model(service_api_ns, EventStreamResponse)
@service_api_ns.route("/workflow/<string:task_id>/events")
class WorkflowEventsApi(Resource):
"""Service API for getting workflow execution events after resume."""
@service_api_ns.doc("get_workflow_events")
@service_api_ns.doc(description="Get workflow execution events stream after resume")
@service_api_ns.doc(
params={
"task_id": "Workflow run ID",
"user": "End user identifier (query param)",
"include_state_snapshot": (
"Whether to replay from persisted state snapshot, "
'specify `"true"` to include a status snapshot of executed nodes'
),
"continue_on_pause": (
"Whether to keep the stream open across workflow_paused events,"
'specify `"true"` to keep the stream open for `workflow_paused` events.'
),
}
)
@service_api_ns.doc(params={"task_id": "Workflow run ID"})
@service_api_ns.doc(params=query_params_from_model(WorkflowEventsQuery))
@service_api_ns.doc(
responses={
200: "SSE event stream",
@ -54,6 +55,7 @@ class WorkflowEventsApi(Resource):
404: "Workflow run not found",
}
)
@service_api_ns.response(200, "SSE event stream", service_api_ns.models[EventStreamResponse.__name__])
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY, required=True))
def get(self, app_model: App, end_user: EndUser, task_id: str):
app_mode = AppMode.value_of(app_model.mode)

View File

@ -57,7 +57,7 @@ class DatasetCreatePayload(BaseModel):
retrieval_model: RetrievalModel | None = None
embedding_model: str | None = None
embedding_model_provider: str | None = None
summary_index_setting: dict | None = None
summary_index_setting: dict | None = Field(default=None)
class DatasetUpdatePayload(BaseModel):
@ -69,11 +69,15 @@ class DatasetUpdatePayload(BaseModel):
embedding_model_provider: str | None = None
retrieval_model: RetrievalModel | None = None
partial_member_list: list[dict[str, str]] | None = None
external_retrieval_model: dict[str, Any] | None = None
external_retrieval_model: dict[str, Any] | None = Field(default=None)
external_knowledge_id: str | None = None
external_knowledge_api_id: str | None = None
class DocumentStatusPayload(BaseModel):
document_ids: list[str] = Field(default_factory=list, description="Document IDs to update")
class TagNamePayload(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
@ -175,6 +179,7 @@ register_schema_models(
service_api_ns,
DatasetCreatePayload,
DatasetUpdatePayload,
DocumentStatusPayload,
TagCreatePayload,
TagUpdatePayload,
TagDeletePayload,
@ -535,6 +540,7 @@ class DocumentStatusApi(DatasetApiResource):
400: "Bad request - invalid action",
}
)
@service_api_ns.expect(service_api_ns.models[DocumentStatusPayload.__name__])
def patch(self, tenant_id, dataset_id: UUID, action: Literal["enable", "disable", "archive", "un_archive"]):
"""
Batch update document status.

View File

@ -8,7 +8,7 @@ deprecated in generated API docs so clients migrate toward the canonical paths.
import json
from collections.abc import Mapping
from contextlib import ExitStack
from typing import Self
from typing import Any, Literal, Self
from uuid import UUID
from flask import request, send_file
@ -25,7 +25,7 @@ from controllers.common.errors import (
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.common.fields import UrlResponse
from controllers.common.fields import BinaryFileResponse, UrlResponse
from controllers.common.schema import (
query_params_from_model,
register_enum_models,
@ -51,6 +51,7 @@ from extensions.ext_database import db
from fields.base import ResponseModel
from fields.document_fields import (
DocumentListResponse,
DocumentMetadataResponse,
DocumentResponse,
DocumentStatusListResponse,
)
@ -117,6 +118,10 @@ class DocumentListQuery(BaseModel):
status: str | None = Field(default=None, description="Document status filter")
class DocumentGetQuery(BaseModel):
metadata: Literal["all", "only", "without"] = Field(default="all", description="Metadata response mode")
DOCUMENT_CREATE_BY_FILE_PARAMS = {
"dataset_id": "Dataset ID",
"file": {
@ -155,6 +160,40 @@ class DocumentAndBatchResponse(ResponseModel):
batch: str
class DocumentDetailResponse(ResponseModel):
id: str
position: int | None = None
data_source_type: str | None = None
data_source_info: dict[str, Any] | None = Field(default=None)
dataset_process_rule_id: str | None = None
dataset_process_rule: dict[str, Any] | None = Field(default=None)
document_process_rule: dict[str, Any] | None = Field(default=None)
name: str | None = None
created_from: str | None = None
created_by: str | None = None
created_at: int | None = None
tokens: int | None = None
indexing_status: str | None = None
completed_at: int | None = None
updated_at: int | None = None
indexing_latency: float | None = None
error: str | None = None
enabled: bool | None = None
disabled_at: int | None = None
disabled_by: str | None = None
archived: bool | None = None
doc_type: str | None = None
doc_metadata: list[DocumentMetadataResponse] | None = None
segment_count: int | None = None
average_segment_length: float | None = None
hit_count: int | None = None
display_status: str | None = None
doc_form: str | None = None
doc_language: str | None = None
summary_index_status: str | None = None
need_summary: bool | None = None
register_enum_models(service_api_ns, RetrievalMethod)
register_schema_models(
@ -164,6 +203,7 @@ register_schema_models(
DocumentTextCreatePayload,
DocumentTextUpdate,
DocumentListQuery,
DocumentGetQuery,
DocumentBatchDownloadZipPayload,
Rule,
PreProcessingRule,
@ -171,9 +211,11 @@ register_schema_models(
)
register_response_schema_models(
service_api_ns,
BinaryFileResponse,
UrlResponse,
DocumentResponse,
DocumentAndBatchResponse,
DocumentDetailResponse,
DocumentListResponse,
DocumentStatusListResponse,
)
@ -716,6 +758,11 @@ class DocumentBatchDownloadZipApi(DatasetApiResource):
404: "Document or dataset not found",
}
)
@service_api_ns.response(
200,
"ZIP archive generated successfully",
service_api_ns.models[BinaryFileResponse.__name__],
)
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id: UUID):
payload = DocumentBatchDownloadZipPayload.model_validate(service_api_ns.payload or {})
@ -851,6 +898,7 @@ class DocumentApi(DatasetApiResource):
@service_api_ns.doc("get_document")
@service_api_ns.doc(description="Get a specific document by ID")
@service_api_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
@service_api_ns.doc(params=query_params_from_model(DocumentGetQuery))
@service_api_ns.doc(
responses={
200: "Document retrieved successfully",
@ -859,6 +907,11 @@ class DocumentApi(DatasetApiResource):
404: "Document not found",
}
)
@service_api_ns.response(
200,
"Document retrieved successfully",
service_api_ns.models[DocumentDetailResponse.__name__],
)
def get(self, tenant_id, dataset_id: UUID, document_id: UUID):
dataset_id_str = str(dataset_id)
document_id_str = str(document_id)

View File

@ -3,19 +3,26 @@ from typing import Any
from uuid import UUID
from flask import request
from pydantic import BaseModel
from pydantic import BaseModel, Field, RootModel
from sqlalchemy import select
from werkzeug.exceptions import Forbidden, NotFound
import services
from controllers.common.errors import FilenameNotExistsError, NoFileUploadedError, TooManyFilesError
from controllers.common.schema import register_schema_model
from controllers.common.fields import GeneratedAppResponse
from controllers.common.schema import (
query_params_from_model,
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.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.login import current_user
from models import Account
@ -38,8 +45,50 @@ class DatasourceNodeRunPayload(BaseModel):
is_published: bool
class DatasourcePluginsQuery(BaseModel):
is_published: bool = True
class DatasourceCredentialInfoResponse(ResponseModel):
id: str | None = None
name: str | None = None
type: str | None = None
is_default: bool | None = None
class DatasourcePluginResponse(ResponseModel):
node_id: str | None = None
plugin_id: str | None = None
provider_name: str | None = None
datasource_type: str | None = None
title: str | None = None
user_input_variables: list[dict[str, Any]] = Field(default_factory=list)
credentials: list[DatasourceCredentialInfoResponse]
class DatasourcePluginListResponse(RootModel[list[DatasourcePluginResponse]]):
pass
class PipelineUploadFileResponse(ResponseModel):
id: str
name: str
size: int
extension: str
mime_type: str | None = None
created_by: str
created_at: str | None = None
register_schema_model(service_api_ns, DatasourceNodeRunPayload)
register_schema_model(service_api_ns, PipelineRunApiEntity)
register_schema_models(service_api_ns, DatasourcePluginsQuery)
register_response_schema_models(
service_api_ns,
DatasourcePluginListResponse,
GeneratedAppResponse,
PipelineUploadFileResponse,
)
@service_api_ns.route("/datasets/<uuid:dataset_id>/pipeline/datasource-plugins")
@ -53,18 +102,18 @@ class DatasourcePluginsApi(DatasetApiResource):
"dataset_id": "Dataset ID",
}
)
@service_api_ns.doc(
params={
"is_published": "Whether to get published or draft datasource plugins "
"(true for published, false for draft, default: true)"
}
)
@service_api_ns.doc(params=query_params_from_model(DatasourcePluginsQuery))
@service_api_ns.doc(
responses={
200: "Datasource plugins retrieved successfully",
401: "Unauthorized - invalid API token",
}
)
@service_api_ns.response(
200,
"Datasource plugins retrieved successfully",
service_api_ns.models[DatasourcePluginListResponse.__name__],
)
def get(self, tenant_id: str, dataset_id: UUID):
"""Resource for getting datasource plugins."""
dataset_id_str = str(dataset_id)
@ -95,15 +144,6 @@ class DatasourceNodeRunApi(DatasetApiResource):
"dataset_id": "Dataset ID",
}
)
@service_api_ns.doc(
body={
"inputs": "User input variables",
"datasource_type": "Datasource type, e.g. online_document",
"credential_id": "Credential ID",
"is_published": "Whether to get published or draft datasource plugins "
"(true for published, false for draft, default: true)",
}
)
@service_api_ns.doc(
responses={
200: "Datasource node run successfully",
@ -111,6 +151,11 @@ 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)
@ -157,17 +202,6 @@ class PipelineRunApi(DatasetApiResource):
"dataset_id": "Dataset ID",
}
)
@service_api_ns.doc(
body={
"inputs": "User input variables",
"datasource_type": "Datasource type, e.g. online_document",
"datasource_info_list": "Datasource info list",
"start_node_id": "Start node ID",
"is_published": "Whether to get published or draft datasource plugins "
"(true for published, false for draft, default: true)",
"streaming": "Whether to stream the response(streaming or blocking), default: streaming",
}
)
@service_api_ns.doc(
responses={
200: "Pipeline run successfully",
@ -175,6 +209,11 @@ 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)
@ -220,6 +259,11 @@ class KnowledgebasePipelineFileUploadApi(DatasetApiResource):
415: "Unsupported file type",
}
)
@service_api_ns.response(
201,
"File uploaded successfully",
service_api_ns.models[PipelineUploadFileResponse.__name__],
)
def post(self, tenant_id: str):
"""Upload a file for use in conversations.

View File

@ -1,12 +1,22 @@
from flask_login import current_user
from flask_restx import Resource
from controllers.common.schema import register_response_schema_models
from controllers.service_api import service_api_ns
from controllers.service_api.wraps import validate_dataset_token
from fields.base import ResponseModel
from graphon.model_runtime.utils.encoders import jsonable_encoder
from services.entities.model_provider_entities import ProviderWithModelsResponse
from services.model_provider_service import ModelProviderService
class ProviderWithModelsListResponse(ResponseModel):
data: list[ProviderWithModelsResponse]
register_response_schema_models(service_api_ns, ProviderWithModelsListResponse)
@service_api_ns.route("/workspaces/current/models/model-types/<string:model_type>")
class ModelProviderAvailableModelApi(Resource):
@service_api_ns.doc("get_available_models")
@ -18,6 +28,11 @@ class ModelProviderAvailableModelApi(Resource):
401: "Unauthorized - invalid API token",
}
)
@service_api_ns.response(
200,
"Models retrieved successfully",
service_api_ns.models[ProviderWithModelsListResponse.__name__],
)
@validate_dataset_token
def get(self, _, model_type: str):
"""Get available models by model type.

View File

@ -8,7 +8,7 @@ from werkzeug.exceptions import Unauthorized
from constants import HEADER_NAME_APP_CODE
from controllers.common import fields
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from libs.passport import PassportService
from libs.token import extract_webapp_passport
@ -32,9 +32,24 @@ class AppAccessModeQuery(BaseModel):
app_code: str | None = Field(default=None, alias="appCode", description="Application code")
register_schema_models(web_ns, AppAccessModeQuery)
class AppPermissionQuery(BaseModel):
model_config = ConfigDict(populate_by_name=True)
app_id: str = Field(..., alias="appId", description="Application ID")
class AppMetaResponse(BaseModel):
tool_icons: dict[str, Any] = Field(
default_factory=dict,
description="Tool icon metadata keyed by tool name",
)
register_schema_models(web_ns, AppAccessModeQuery, AppPermissionQuery)
register_response_schema_models(
web_ns,
fields.Parameters,
AppMetaResponse,
fields.AccessModeResponse,
fields.BooleanResultResponse,
)
@ -56,6 +71,7 @@ class AppParameterApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[fields.Parameters.__name__])
def get(self, app_model: App, end_user: EndUser):
"""Retrieve app parameters."""
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
@ -92,6 +108,7 @@ class AppMeta(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[AppMetaResponse.__name__])
def get(self, app_model: App, end_user: EndUser):
"""Get app meta"""
return AppService().get_app_meta(app_model)
@ -101,12 +118,7 @@ class AppMeta(WebApiResource):
class AppAccessMode(Resource):
@web_ns.doc("Get App Access Mode")
@web_ns.doc(description="Retrieve the access mode for a web application (public or restricted).")
@web_ns.doc(
params={
"appId": {"description": "Application ID", "type": "string", "required": False},
"appCode": {"description": "Application code", "type": "string", "required": False},
}
)
@web_ns.doc(params=query_params_from_model(AppAccessModeQuery))
@web_ns.doc(
responses={
200: "Success",
@ -139,7 +151,7 @@ class AppAccessMode(Resource):
class AppWebAuthPermission(Resource):
@web_ns.doc("Check App Permission")
@web_ns.doc(description="Check if user has permission to access a web application.")
@web_ns.doc(params={"appId": {"description": "Application ID", "type": "string", "required": True}})
@web_ns.doc(params=query_params_from_model(AppPermissionQuery))
@web_ns.doc(
responses={
200: "Success",

View File

@ -7,6 +7,7 @@ from werkzeug.exceptions import InternalServerError
import services
from controllers.common.controller_schemas import TextToAudioPayload as TextToAudioPayloadBase
from controllers.common.fields import AudioBinaryResponse, AudioTranscriptResponse
from controllers.web import web_ns
from controllers.web.error import (
AppUnavailableError,
@ -32,7 +33,7 @@ from services.errors.audio import (
UnsupportedAudioTypeServiceError,
)
from ..common.schema import register_schema_models
from ..common.schema import register_response_schema_models, register_schema_models
class TextToAudioPayload(TextToAudioPayloadBase):
@ -45,6 +46,7 @@ class TextToAudioPayload(TextToAudioPayloadBase):
register_schema_models(web_ns, TextToAudioPayload)
register_response_schema_models(web_ns, AudioBinaryResponse, AudioTranscriptResponse)
logger = logging.getLogger(__name__)
@ -69,6 +71,7 @@ class AudioApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[AudioTranscriptResponse.__name__])
def post(self, app_model: App, end_user: EndUser):
"""Convert audio to text"""
file = request.files["file"]
@ -117,6 +120,7 @@ class TextApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[AudioBinaryResponse.__name__])
def post(self, app_model: App, end_user: EndUser):
"""Convert text to audio"""
try:

View File

@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import (
@ -47,9 +47,14 @@ def _resolve_agent_app_streaming(*, app_mode: AppMode, response_mode: str | None
class CompletionMessagePayload(BaseModel):
inputs: dict[str, Any] = Field(description="Input variables for the completion")
inputs: dict[str, Any] = Field(
description="Input variables for the completion",
)
query: str = Field(default="", description="Query text for completion")
files: list[dict[str, Any]] | None = Field(default=None, description="Files to be processed")
files: list[dict[str, Any]] | None = Field(
default=None,
description="Files to be processed",
)
response_mode: Literal["blocking", "streaming"] | None = Field(
default=None, description="Response mode: blocking or streaming"
)
@ -57,9 +62,14 @@ class CompletionMessagePayload(BaseModel):
class ChatMessagePayload(BaseModel):
inputs: dict[str, Any] = Field(description="Input variables for the chat")
inputs: dict[str, Any] = Field(
description="Input variables for the chat",
)
query: str = Field(description="User query/message")
files: list[dict[str, Any]] | None = Field(default=None, description="Files to be processed")
files: list[dict[str, Any]] | None = Field(
default=None,
description="Files to be processed",
)
response_mode: Literal["blocking", "streaming"] | None = Field(
default=None, description="Response mode: blocking or streaming"
)
@ -76,7 +86,7 @@ class ChatMessagePayload(BaseModel):
register_schema_models(web_ns, CompletionMessagePayload, ChatMessagePayload)
register_response_schema_models(web_ns, SimpleResultResponse)
register_response_schema_models(web_ns, GeneratedAppResponse, SimpleResultResponse)
# define completion api for user
@ -95,6 +105,7 @@ class CompletionApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
def post(self, app_model: App, end_user: EndUser):
if app_model.mode != AppMode.COMPLETION:
raise NotCompletionAppError()
@ -178,6 +189,7 @@ class ChatApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
def post(self, app_model: App, end_user: EndUser):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:

View File

@ -7,7 +7,7 @@ from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import NotFound
from controllers.common.controller_schemas import ConversationRenamePayload
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import NotChatAppError
from controllers.web.wraps import WebApiResource
@ -40,37 +40,14 @@ class ConversationListQuery(BaseModel):
register_schema_models(web_ns, ConversationListQuery, ConversationRenamePayload)
register_response_schema_models(web_ns, ResultResponse)
register_response_schema_models(web_ns, ConversationInfiniteScrollPagination, ResultResponse, SimpleConversation)
@web_ns.route("/conversations")
class ConversationListApi(WebApiResource):
@web_ns.doc("Get Conversation List")
@web_ns.doc(description="Retrieve paginated list of conversations for a chat application.")
@web_ns.doc(
params={
"last_id": {"description": "Last conversation ID for pagination", "type": "string", "required": False},
"limit": {
"description": "Number of conversations to return (1-100)",
"type": "integer",
"required": False,
"default": 20,
},
"pinned": {
"description": "Filter by pinned status",
"type": "string",
"enum": ["true", "false"],
"required": False,
},
"sort_by": {
"description": "Sort order",
"type": "string",
"enum": ["created_at", "-created_at", "updated_at", "-updated_at"],
"required": False,
"default": "-updated_at",
},
}
)
@web_ns.doc(params=query_params_from_model(ConversationListQuery))
@web_ns.doc(
responses={
200: "Success",
@ -81,6 +58,7 @@ class ConversationListApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[ConversationInfiniteScrollPagination.__name__])
def get(self, app_model: App, end_user: EndUser):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
@ -166,6 +144,8 @@ class ConversationRenameApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Conversation renamed successfully", web_ns.models[SimpleConversation.__name__])
@web_ns.expect(web_ns.models[ConversationRenamePayload.__name__])
def post(self, app_model: App, end_user: EndUser, c_id: UUID):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:

View File

@ -9,7 +9,7 @@ from typing import Any, NotRequired, TypedDict
from flask import Response, request
from flask_restx import Resource
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import select
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden
@ -17,7 +17,7 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.common.errors import NotFoundError
from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values
from controllers.common.schema import register_schema_models
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import WebFormRateLimitExceededError
from controllers.web.site import serialize_app_site_payload
@ -38,7 +38,26 @@ class HumanInputUploadTokenResponse(BaseModel):
expires_at: int
register_schema_models(web_ns, HumanInputUploadTokenResponse)
class HumanInputFormDefinitionResponse(BaseModel):
form_content: Any
inputs: Any
resolved_default_values: dict[str, str]
user_actions: Any
expiration_time: int
site: dict[str, Any] | None = Field(default=None)
class HumanInputFormSubmitResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
register_schema_models(web_ns, HumanInputFormSubmitPayload)
register_response_schema_models(
web_ns,
HumanInputUploadTokenResponse,
HumanInputFormDefinitionResponse,
HumanInputFormSubmitResponse,
)
_FORM_SUBMIT_RATE_LIMITER = RateLimiter(
@ -100,6 +119,7 @@ def _jsonify_form_definition(
class HumanInputFormUploadTokenApi(Resource):
"""API for issuing HITL upload tokens for active human input forms."""
@web_ns.response(200, "Success", web_ns.models[HumanInputUploadTokenResponse.__name__])
def post(self, form_token: str):
"""
Issue an upload token for a human input form.
@ -130,6 +150,7 @@ class HumanInputFormApi(Resource):
# NOTE(QuantumGhost): this endpoint is unauthenticated on purpose for now.
# def get(self, _app_model: App, _end_user: EndUser, form_token: str):
@web_ns.response(200, "Success", web_ns.models[HumanInputFormDefinitionResponse.__name__])
def get(self, form_token: str):
"""
Get human input form definition by token.
@ -160,6 +181,8 @@ class HumanInputFormApi(Resource):
)
# def post(self, _app_model: App, _end_user: EndUser, form_token: str):
@web_ns.response(200, "Success", web_ns.models[HumanInputFormSubmitResponse.__name__])
@web_ns.expect(web_ns.models[HumanInputFormSubmitPayload.__name__])
def post(self, form_token: str):
"""
Submit human input form by token.

View File

@ -14,7 +14,7 @@ from controllers.common.fields import (
SimpleResultDataResponse,
SimpleResultResponse,
)
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console.auth.error import (
AuthenticationFailedError,
EmailCodeError,
@ -62,7 +62,12 @@ class EmailCodeLoginVerifyPayload(BaseModel):
token: str = Field(min_length=1)
register_schema_models(web_ns, LoginPayload, EmailCodeLoginSendPayload, EmailCodeLoginVerifyPayload)
class LoginStatusQuery(BaseModel):
app_code: str | None = Field(default=None, description="Web app code")
user_id: str | None = Field(default=None, description="End user session ID")
register_schema_models(web_ns, LoginPayload, EmailCodeLoginSendPayload, EmailCodeLoginVerifyPayload, LoginStatusQuery)
register_response_schema_models(
web_ns,
AccessTokenResultResponse,
@ -122,6 +127,7 @@ class LoginStatusApi(Resource):
@setup_required
@web_ns.doc("web_app_login_status")
@web_ns.doc(description="Check login status")
@web_ns.doc(params=query_params_from_model(LoginStatusQuery))
@web_ns.doc(
responses={
200: "Login status",

View File

@ -7,6 +7,7 @@ from pydantic import BaseModel, Field, TypeAdapter
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery
from controllers.common.fields import GeneratedAppResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import (
@ -48,29 +49,20 @@ class MessageMoreLikeThisQuery(BaseModel):
register_schema_models(web_ns, MessageListQuery, MessageFeedbackPayload, MessageMoreLikeThisQuery)
register_response_schema_models(web_ns, ResultResponse, SuggestedQuestionsResponse)
register_response_schema_models(
web_ns,
GeneratedAppResponse,
ResultResponse,
SuggestedQuestionsResponse,
WebMessageInfiniteScrollPagination,
)
@web_ns.route("/messages")
class MessageListApi(WebApiResource):
@web_ns.doc("Get Message List")
@web_ns.doc(description="Retrieve paginated list of messages from a conversation in a chat application.")
@web_ns.doc(
params={
"conversation_id": {"description": "Conversation UUID", "type": "string", "required": True},
"first_id": {
"description": "First message ID for pagination",
"type": "string",
"required": False,
},
"limit": {
"description": "Number of messages to return (1-100)",
"type": "integer",
"required": False,
"default": 20,
},
}
)
@web_ns.doc(params=query_params_from_model(MessageListQuery))
@web_ns.doc(
responses={
200: "Success",
@ -81,6 +73,7 @@ class MessageListApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[WebMessageInfiniteScrollPagination.__name__])
def get(self, app_model: App, end_user: EndUser):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
@ -133,6 +126,7 @@ class MessageFeedbackApi(WebApiResource):
}
)
@web_ns.response(200, "Feedback submitted successfully", web_ns.models[ResultResponse.__name__])
@web_ns.expect(web_ns.models[MessageFeedbackPayload.__name__])
def post(self, app_model: App, end_user: EndUser, message_id: UUID):
message_id_str = str(message_id)
@ -167,6 +161,7 @@ class MessageMoreLikeThisApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
def get(self, app_model: App, end_user: EndUser, message_id: UUID):
if app_model.mode != "completion":
raise NotCompletionAppError()

View File

@ -4,11 +4,14 @@ from typing import Any
from flask import make_response, request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy import func, select
from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config
from constants import HEADER_NAME_APP_CODE
from controllers.common.fields import AccessTokenData
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import WebAppAuthRequiredError
from extensions.ext_database import db
@ -19,12 +22,21 @@ from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
class PassportQuery(BaseModel):
user_id: str | None = Field(default=None, description="End user session ID")
register_schema_models(web_ns, PassportQuery)
register_response_schema_models(web_ns, AccessTokenData)
@web_ns.route("/passport")
class PassportResource(Resource):
"""Base resource for passport."""
@web_ns.doc("get_passport")
@web_ns.doc(description="Get authentication passport for web application access")
@web_ns.doc(params=query_params_from_model(PassportQuery))
@web_ns.doc(
responses={
200: "Passport retrieved successfully",
@ -32,6 +44,7 @@ class PassportResource(Resource):
404: "Application or user not found",
}
)
@web_ns.response(200, "Passport retrieved successfully", web_ns.models[AccessTokenData.__name__])
def get(self):
system_features = FeatureService.get_system_features()
app_code = request.headers.get(HEADER_NAME_APP_CODE)

View File

@ -86,6 +86,7 @@ class RemoteFileUploadApi(WebApiResource):
}
)
@web_ns.response(201, "Remote file uploaded", web_ns.models[FileWithSignedUrl.__name__])
@web_ns.expect(web_ns.models[RemoteFileUploadPayload.__name__])
def post(self, app_model: App, end_user: EndUser):
"""Upload a file from a remote URL.

View File

@ -5,7 +5,7 @@ from pydantic import TypeAdapter
from werkzeug.exceptions import NotFound
from controllers.common.controller_schemas import SavedMessageCreatePayload, SavedMessageListQuery
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import NotCompletionAppError
from controllers.web.wraps import WebApiResource
@ -16,24 +16,14 @@ from services.errors.message import MessageNotExistsError
from services.saved_message_service import SavedMessageService
register_schema_models(web_ns, SavedMessageListQuery, SavedMessageCreatePayload)
register_response_schema_models(web_ns, ResultResponse)
register_response_schema_models(web_ns, ResultResponse, SavedMessageInfiniteScrollPagination)
@web_ns.route("/saved-messages")
class SavedMessageListApi(WebApiResource):
@web_ns.doc("Get Saved Messages")
@web_ns.doc(description="Retrieve paginated list of saved messages for a completion application.")
@web_ns.doc(
params={
"last_id": {"description": "Last message ID for pagination", "type": "string", "required": False},
"limit": {
"description": "Number of messages to return (1-100)",
"type": "integer",
"required": False,
"default": 20,
},
}
)
@web_ns.doc(params=query_params_from_model(SavedMessageListQuery))
@web_ns.doc(
responses={
200: "Success",
@ -44,6 +34,7 @@ class SavedMessageListApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[SavedMessageInfiniteScrollPagination.__name__])
def get(self, app_model: App, end_user: EndUser):
if app_model.mode != "completion":
raise NotCompletionAppError()
@ -78,6 +69,7 @@ class SavedMessageListApi(WebApiResource):
}
)
@web_ns.response(200, "Message saved successfully", web_ns.models[ResultResponse.__name__])
@web_ns.expect(web_ns.models[SavedMessageCreatePayload.__name__])
def post(self, app_model: App, end_user: EndUser):
if app_model.mode != "completion":
raise NotCompletionAppError()

View File

@ -1,19 +1,64 @@
from typing import Any, cast
from flask_restx import fields, marshal, marshal_with
from pydantic import Field
from sqlalchemy import select
from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.common.schema import register_response_schema_models
from controllers.web import web_ns
from controllers.web.wraps import WebApiResource
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import AppIconUrlField
from models.account import TenantStatus
from models.model import App, EndUser, Site
from services.feature_service import FeatureService
class AppSiteModelConfigResponse(ResponseModel):
opening_statement: str | None = None
suggested_questions: Any
suggested_questions_after_answer: Any
more_like_this: Any
model: Any
user_input_form: Any
pre_prompt: str | None = None
class AppSiteResponse(ResponseModel):
title: str | None = None
chat_color_theme: str | None = None
chat_color_theme_inverted: bool | None = None
icon_type: str | None = None
icon: str | None = None
icon_background: str | None = None
icon_url: str | None = None
description: str | None = None
copyright: str | None = None
privacy_policy: str | None = None
custom_disclaimer: str | None = None
default_language: str | None = None
prompt_public: bool | None = None
show_workflow_steps: bool | None = None
use_icon_as_answer_icon: bool | None = None
class AppSiteInfoResponse(ResponseModel):
app_id: str
end_user_id: str | None = None
enable_site: bool
site: AppSiteResponse
model_config_: AppSiteModelConfigResponse | None = Field(default=None, alias="model_config")
plan: str | None = None
can_replace_logo: bool
custom_config: dict[str, Any] | None = Field(default=None)
register_response_schema_models(web_ns, AppSiteInfoResponse)
@web_ns.route("/site")
class AppSiteApi(WebApiResource):
"""Resource for app sites."""
@ -69,6 +114,7 @@ class AppSiteApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[AppSiteInfoResponse.__name__])
@marshal_with(app_fields)
def get(self, app_model: App, end_user: EndUser):
"""Retrieve app site info."""

View File

@ -3,7 +3,7 @@ import logging
from werkzeug.exceptions import InternalServerError
from controllers.common.controller_schemas import WorkflowRunPayload
from controllers.common.fields import SimpleResultResponse
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import (
@ -33,7 +33,7 @@ from services.errors.llm import InvokeRateLimitError
logger = logging.getLogger(__name__)
register_schema_models(web_ns, WorkflowRunPayload)
register_response_schema_models(web_ns, SimpleResultResponse)
register_response_schema_models(web_ns, GeneratedAppResponse, SimpleResultResponse)
@web_ns.route("/workflows/run")
@ -51,6 +51,7 @@ class WorkflowRunApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
def post(self, app_model: App, end_user: EndUser):
"""
Run workflow

View File

@ -9,7 +9,9 @@ from flask import Response, request
from sqlalchemy.orm import sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.web import api
from controllers.common.fields import EventStreamResponse
from controllers.common.schema import register_response_schema_model
from controllers.web import api, web_ns
from controllers.web.wraps import WebApiResource
from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator
from core.app.apps.base_app_generator import BaseAppGenerator
@ -22,10 +24,13 @@ from models.model import App, AppMode, EndUser
from repositories.factory import DifyAPIRepositoryFactory
from services.workflow_event_snapshot_service import build_workflow_event_stream
register_response_schema_model(web_ns, EventStreamResponse)
class WorkflowEventsApi(WebApiResource):
"""API for getting workflow execution events after resume."""
@web_ns.response(200, "SSE event stream", web_ns.models[EventStreamResponse.__name__])
def get(self, app_model: App, end_user: EndUser, task_id: str):
"""
Get workflow execution events stream after resume.

View File

@ -1,10 +1,11 @@
from collections.abc import Sequence
from enum import StrEnum, auto
from typing import Any
from pydantic import BaseModel, ConfigDict
from graphon.model_runtime.entities.common_entities import I18nObject
from graphon.model_runtime.entities.model_entities import ModelType, ProviderModel
from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType, ProviderModel
from graphon.model_runtime.entities.provider_entities import ProviderEntity
@ -52,6 +53,7 @@ class ProviderModelWithStatusEntity(ProviderModel):
Model class for model response.
"""
model_properties: dict[ModelPropertyKey, Any]
status: ModelStatus
load_balancing_enabled: bool = False
has_invalid_load_balancing_configs: bool = False

Some files were not shown because too many files have changed in this diff Show More