mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 05:41:09 +08:00
1333 lines
40 KiB
Python
1333 lines
40 KiB
Python
"""Shared response substructures for openapi endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Any, Final, Literal, Protocol, Self
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator, model_validator
|
|
|
|
from constants.languages import supported_language
|
|
from constants.oauth_bearer import SubjectType
|
|
from controllers.common.human_input import HumanInputFormSubmitPayload
|
|
from controllers.openapi._i18n import localized
|
|
from controllers.openapi._upload import UploadPart, UploadParts
|
|
from core.plugin.entities.plugin import PluginCategory
|
|
from core.plugin.entities.plugin_daemon import PluginInstallTaskStatus
|
|
from core.tools.entities.tool_entities import ToolProviderType
|
|
from enums import DeploymentEdition, WebAppAccessMode
|
|
from fields.workflow_run_fields import WorkflowRunPaginationResponse
|
|
from graphon.enums import BuiltinNodeTypes, WorkflowExecutionStatus
|
|
from graphon.model_runtime.entities.model_entities import ModelType
|
|
from graphon.nodes.tool.entities import ToolInputType
|
|
from graphon.variables import SegmentType
|
|
from libs.helper import EmailStr, UUIDStr, UUIDStrOrEmpty, to_timestamp, uuid_value
|
|
from models.enums import WorkflowRunTriggeredFrom
|
|
from models.model import AppMode, IconType
|
|
from services.entities.app_entities import SupportedAppType
|
|
from services.entities.dsl_entities import CheckDependenciesResult, Import, ImportMode
|
|
from services.workflow.graph_check import GraphIssue
|
|
from services.workflow.graph_diff import WorkflowDiff
|
|
|
|
# Server-side cap on `limit` query param for /openapi/v1/* list endpoints.
|
|
MAX_PAGE_LIMIT = 100
|
|
|
|
|
|
class UsageInfo(BaseModel):
|
|
prompt_tokens: int = 0
|
|
completion_tokens: int = 0
|
|
total_tokens: int = 0
|
|
|
|
|
|
class MessageMetadata(BaseModel):
|
|
usage: UsageInfo | None = None
|
|
retriever_resources: list[dict[str, Any]] = []
|
|
|
|
|
|
class Hint(BaseModel):
|
|
"""A next step: the op id to call and its ready input."""
|
|
|
|
summary: str
|
|
op: str
|
|
input: dict[str, Any] = Field(description="Ready-to-send input for `op`; unknown values are null")
|
|
form: list[dict[str, Any]] | None = Field(
|
|
default=None, description="Form fields behind the hint's input: a paused run's form inputs, or credentials"
|
|
)
|
|
|
|
|
|
class Hinted(BaseModel):
|
|
"""The one place a response carries server-built next steps; `hints` is reserved on every op input."""
|
|
|
|
hints: list[Hint] = Field(default_factory=list, description="Next steps the caller can take")
|
|
|
|
|
|
class PageQuery(BaseModel):
|
|
"""The two query parameters every list op takes; the next-page hint is built from this model."""
|
|
|
|
page: int = Field(1, ge=1)
|
|
limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT)
|
|
|
|
|
|
class PaginationEnvelope[T](Hinted):
|
|
"""The one shape every paginated list on this surface answers with."""
|
|
|
|
page: int
|
|
limit: int
|
|
total: int
|
|
has_more: bool
|
|
data: list[T]
|
|
|
|
@classmethod
|
|
def build(cls, *, page: int, limit: int, total: int, items: list[T]) -> Self:
|
|
return cls(page=page, limit=limit, total=total, has_more=page * limit < total, data=items)
|
|
|
|
@classmethod
|
|
def page_of(cls, items: list[T], *, query: PageQuery) -> Self:
|
|
"""The page `query` asks for, cut from a list the service returned whole."""
|
|
start = (query.page - 1) * query.limit
|
|
return cls.build(page=query.page, limit=query.limit, total=len(items), items=items[start : start + query.limit])
|
|
|
|
|
|
class AppListRow(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: str | None = None
|
|
mode: AppMode
|
|
updated_at: str | None = None
|
|
workspace_id: str | None = None
|
|
workspace_name: str | None = None
|
|
|
|
|
|
class AppListResponse(PaginationEnvelope[AppListRow]):
|
|
pass
|
|
|
|
|
|
class PermittedExternalAppsListResponse(PaginationEnvelope[AppListRow]):
|
|
pass
|
|
|
|
|
|
class AppInfo(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: str | None = None
|
|
mode: str
|
|
|
|
|
|
class AppDescribeInfo(AppInfo):
|
|
updated_at: str | None = None
|
|
service_api_enabled: bool
|
|
is_agent: bool = Field(False, description="True for agent-chat and advanced-chat apps")
|
|
|
|
|
|
class AppDescribeResponse(Hinted):
|
|
info: AppDescribeInfo | None = None
|
|
parameters: dict[str, Any] | None = Field(default=None)
|
|
input_schema: dict[str, Any] | None = Field(default=None)
|
|
|
|
|
|
class WorkflowRunData(BaseModel):
|
|
id: str
|
|
workflow_id: str
|
|
status: str
|
|
outputs: dict[str, Any] = Field(default_factory=dict)
|
|
error: str | None = None
|
|
elapsed_time: float | None = None
|
|
total_tokens: int | None = None
|
|
total_steps: int | None = None
|
|
created_at: int | None = None
|
|
finished_at: int | None = None
|
|
|
|
|
|
class AccountPayload(BaseModel):
|
|
id: str
|
|
email: str
|
|
name: str
|
|
|
|
|
|
class WorkspacePayload(BaseModel):
|
|
id: str
|
|
name: str
|
|
role: str
|
|
|
|
|
|
class DeviceTokenResponse(BaseModel):
|
|
token: str
|
|
expires_at: str
|
|
subject_type: SubjectType
|
|
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: SubjectType
|
|
subject_email: str | None = None
|
|
subject_issuer: str | None = None
|
|
account: AccountPayload | None = None
|
|
workspaces: list[WorkspacePayload] = []
|
|
default_workspace_id: str | None = None
|
|
|
|
|
|
class SessionRow(BaseModel):
|
|
id: str
|
|
prefix: str
|
|
client_id: str
|
|
device_label: str
|
|
created_at: str | None = None
|
|
last_used_at: str | None = None
|
|
expires_at: str | None = None
|
|
|
|
|
|
class SessionListResponse(PaginationEnvelope[SessionRow]):
|
|
pass
|
|
|
|
|
|
class SessionListQuery(PageQuery):
|
|
"""Pagination for GET /account/sessions. Strict (extra='forbid')."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
limit: int = Field(100, ge=1, le=MAX_PAGE_LIMIT)
|
|
|
|
|
|
class RevokeResponse(BaseModel):
|
|
status: str
|
|
|
|
|
|
class WorkspaceSummaryResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
role: str
|
|
status: str
|
|
|
|
|
|
class WorkspaceListResponse(PaginationEnvelope[WorkspaceSummaryResponse]):
|
|
pass
|
|
|
|
|
|
class WorkspaceListQuery(PageQuery):
|
|
"""Strict (extra='forbid')."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class WorkspaceDetailResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
role: str
|
|
status: str
|
|
created_at: str | None = None
|
|
|
|
|
|
class DeviceFlowErrorResponse(BaseModel):
|
|
"""RFC 8628-style device-flow error. These routes keep the OAuth ``{"error": code}`` shape
|
|
instead of ``ErrorBody`` because device clients parse it."""
|
|
|
|
error: Literal[
|
|
"unsupported_client",
|
|
"slow_down",
|
|
"expired_token",
|
|
"authorization_pending",
|
|
"access_denied",
|
|
"expired_or_unknown",
|
|
"already_resolved",
|
|
"approve_in_progress",
|
|
"approval_outcome_unknown",
|
|
"state_lost",
|
|
]
|
|
|
|
|
|
class DeviceCodeResponse(BaseModel):
|
|
device_code: str
|
|
user_code: str
|
|
verification_uri: str
|
|
expires_in: int
|
|
interval: int
|
|
|
|
|
|
class DeviceLookupResponse(BaseModel):
|
|
valid: bool
|
|
expires_in_remaining: int = 0
|
|
client_id: str | None = None
|
|
|
|
|
|
class DeviceMutateResponse(BaseModel):
|
|
status: str
|
|
|
|
|
|
class DeviceApprovalContextResponse(BaseModel):
|
|
subject_email: str
|
|
subject_issuer: str
|
|
user_code: str
|
|
csrf_token: str
|
|
expires_at: datetime
|
|
|
|
|
|
class ServerVersionResponse(BaseModel):
|
|
"""Meta endpoint payload for `GET /openapi/v1/_version` — no auth required."""
|
|
|
|
version: str
|
|
edition: DeploymentEdition
|
|
|
|
|
|
class HealthResponse(BaseModel):
|
|
"""Liveness payload for `GET /openapi/v1/_health` — no auth required."""
|
|
|
|
ok: bool
|
|
|
|
|
|
def _csv_string_query_schema(schema: dict[str, Any]) -> None:
|
|
"""Re-shape a set/list field's query schema to a comma-separated string — the wire form the
|
|
handler actually accepts (`request.args` is flat + the validator splits on ','). Without this
|
|
the generated contract would type it as an array and serialize `fields[0]=…&fields[1]=…`,
|
|
which `extra='forbid'` rejects. Runtime `set[str]` validation is unaffected."""
|
|
schema.pop("anyOf", None)
|
|
schema.pop("items", None)
|
|
schema.pop("uniqueItems", None)
|
|
schema["type"] = "string"
|
|
|
|
|
|
class AppDescribeQuery(BaseModel):
|
|
"""`?fields=` allow-list for GET /apps/<id>.
|
|
|
|
Empty / omitted → all blocks. Unknown member → ValidationError → 422.
|
|
"""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
fields: set[str] | None = Field(default=None, json_schema_extra=_csv_string_query_schema)
|
|
|
|
@field_validator("fields", mode="before")
|
|
@classmethod
|
|
def _parse_fields(cls, v: object) -> set[str] | None:
|
|
if v is None or v == "":
|
|
return None
|
|
if not isinstance(v, str):
|
|
raise ValueError("fields must be a comma-separated string")
|
|
_allowed_describe_fields = frozenset({"info", "parameters", "input_schema"})
|
|
members = {m.strip() for m in v.split(",") if m.strip()}
|
|
unknown = members - _allowed_describe_fields
|
|
if unknown:
|
|
raise ValueError(f"unknown field(s): {sorted(unknown)}")
|
|
return members
|
|
|
|
|
|
class AppListQuery(PageQuery):
|
|
"""mode is a closed enum of listable app types."""
|
|
|
|
workspace_id: UUIDStr
|
|
mode: SupportedAppType | None = None
|
|
name: str | None = Field(
|
|
None,
|
|
max_length=200,
|
|
description="Filter by a name substring; an app id returns that one app and ignores `mode`",
|
|
)
|
|
|
|
|
|
class _ConversationFields(BaseModel):
|
|
conversation_id: UUIDStrOrEmpty | None = Field(default=None, description="Continue an existing conversation")
|
|
auto_generate_name: bool = Field(default=True, description="Let the server name a new conversation")
|
|
|
|
@field_validator("conversation_id", mode="before")
|
|
@classmethod
|
|
def _normalize_conv(cls, value: str | None) -> str | None:
|
|
if isinstance(value, str):
|
|
value = value.strip()
|
|
if not value:
|
|
return None
|
|
try:
|
|
return uuid_value(value)
|
|
except ValueError as exc:
|
|
raise ValueError("conversation_id must be a valid UUID") from exc
|
|
|
|
|
|
class _WorkflowVersionFields(BaseModel):
|
|
workflow_id: str | None = Field(default=None, description="Pin a published workflow version")
|
|
|
|
|
|
class RunPayloadBase(BaseModel):
|
|
"""What every run takes; each mode's payload adds its own fields and forbids the rest."""
|
|
|
|
inputs: dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description=(
|
|
"Variables declared by the app. The exact shape is per app: read `input_schema` from "
|
|
"describe.console_app. A file variable takes a Dify file mapping (remote url or upload id) here, "
|
|
"or a local path in `files`, not both."
|
|
),
|
|
)
|
|
files: UploadParts | None = Field(
|
|
default=None,
|
|
description=(
|
|
"Local file paths keyed by the app's file variable name; each file is uploaded and becomes "
|
|
"that variable's value. Give a list of paths for a file-list variable"
|
|
),
|
|
)
|
|
attachments: list[UploadPart] | None = Field(
|
|
default=None,
|
|
description="Local file paths attached to the run itself (the app's `sys.files`), not to a variable",
|
|
)
|
|
workspace_id: UUIDStrOrEmpty | None = Field(default=None, description="Workspace that owns the app")
|
|
|
|
|
|
class WorkflowRunPayload(RunPayloadBase, _WorkflowVersionFields):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class DraftWorkflowRunPayload(RunPayloadBase):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class ChatRunPayload(RunPayloadBase, _ConversationFields):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
query: str = Field(description="User message")
|
|
|
|
@field_validator("query")
|
|
@classmethod
|
|
def _non_blank(cls, value: str) -> str:
|
|
value = value.strip()
|
|
if not value:
|
|
raise ValueError("query must not be blank")
|
|
return value
|
|
|
|
|
|
class AdvancedChatRunPayload(ChatRunPayload, _WorkflowVersionFields):
|
|
"""A chat run against an advanced-chat (chatflow) app, which can also pin a workflow version."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class CompletionRunPayload(RunPayloadBase):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
query: str = Field(default="", description="Prompt text; most completion apps take their input through `inputs`")
|
|
|
|
|
|
class FileUploadPayload(BaseModel):
|
|
file: UploadPart = Field(
|
|
description="The file to upload; its id can then be used in an app run's file variables",
|
|
)
|
|
|
|
|
|
class DeviceCodeRequest(BaseModel):
|
|
client_id: str
|
|
device_label: str
|
|
|
|
|
|
class DevicePollRequest(BaseModel):
|
|
device_code: str
|
|
client_id: str
|
|
|
|
|
|
class DeviceLookupQuery(BaseModel):
|
|
user_code: str
|
|
|
|
|
|
class DeviceMutateRequest(BaseModel):
|
|
user_code: str
|
|
|
|
|
|
class PermittedExternalAppsListQuery(PageQuery):
|
|
"""Strict (extra='forbid')."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
mode: SupportedAppType | None = None
|
|
name: str | None = Field(None, max_length=200)
|
|
|
|
|
|
# Closed enum for invite/update-role payloads. Owner is intentionally not
|
|
# assignable through these endpoints — ownership transfer goes through the
|
|
# console's three-step email-verification flow.
|
|
MemberAssignableRole = Literal["normal", "admin"]
|
|
|
|
|
|
class MemberResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
email: str
|
|
role: str
|
|
status: str
|
|
avatar: str | None = None
|
|
|
|
|
|
class MemberListResponse(PaginationEnvelope[MemberResponse]):
|
|
pass
|
|
|
|
|
|
class MemberListQuery(PageQuery):
|
|
"""Strict (extra='forbid')."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class MemberInvitePayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
email: EmailStr
|
|
role: MemberAssignableRole
|
|
|
|
|
|
class MemberRoleUpdatePayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
role: MemberAssignableRole
|
|
|
|
|
|
class MemberInviteResponse(BaseModel):
|
|
result: Literal["success"] = "success"
|
|
email: str
|
|
role: str
|
|
member_id: str
|
|
invite_url: str
|
|
tenant_id: str
|
|
|
|
|
|
class MemberActionResponse(BaseModel):
|
|
result: Literal["success"] = "success"
|
|
|
|
|
|
class MarketplacePluginQuery(PageQuery):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
query: str = Field("", description="Words to search for; empty lists the most installed plugins")
|
|
category: PluginCategory | None = Field(None, description="Only plugins of this category")
|
|
|
|
|
|
class MarketplacePluginRow(BaseModel):
|
|
plugin_id: str
|
|
identifier: str = Field(description="Latest versioned id; pass it to install.plugin")
|
|
version: str
|
|
category: str
|
|
label: str | None
|
|
brief: str | None
|
|
authorized_category: str | None = Field(
|
|
description=(
|
|
"Who vouches for the plugin: langgenius (official), partner or community; "
|
|
"workspace install-scope rules check this"
|
|
)
|
|
)
|
|
install_count: int
|
|
installed: bool
|
|
installed_version: str | None
|
|
|
|
|
|
class MarketplacePluginListResponse(PaginationEnvelope[MarketplacePluginRow]):
|
|
pass
|
|
|
|
|
|
class PluginListQuery(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
category: PluginCategory | None = Field(None, description="Only plugins of this category")
|
|
|
|
|
|
class PluginProvides(BaseModel):
|
|
model_provider: str | None = Field(description="Model provider id this plugin adds, for describe.model_provider")
|
|
tool_provider: str | None = Field(description="Tool provider id this plugin adds, for describe.tool_provider")
|
|
|
|
|
|
class PluginRow(BaseModel):
|
|
plugin_id: str
|
|
identifier: str
|
|
version: str
|
|
latest_version: str | None
|
|
category: str
|
|
label: str | None
|
|
source: str
|
|
provides: PluginProvides
|
|
|
|
|
|
class PluginListResponse(Hinted):
|
|
data: list[PluginRow]
|
|
|
|
|
|
class PluginInstallPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
identifiers: list[str] = Field(
|
|
min_length=1,
|
|
description=(
|
|
"Versioned plugin ids such as langgenius/openai:0.2.1@sha256…, from get.marketplace.plugin "
|
|
"(identifier) or check.console_app.dependency"
|
|
),
|
|
)
|
|
|
|
|
|
class PluginTaskStartResponse(Hinted):
|
|
task_id: str
|
|
all_installed: bool
|
|
|
|
|
|
class PluginTaskItem(BaseModel):
|
|
plugin_id: str
|
|
identifier: str
|
|
status: PluginInstallTaskStatus
|
|
message: str
|
|
|
|
|
|
class PluginTaskResponse(Hinted):
|
|
task_id: str
|
|
status: PluginInstallTaskStatus
|
|
plugins: list[PluginTaskItem]
|
|
|
|
|
|
class PluginDeleteResponse(BaseModel):
|
|
plugin_id: str
|
|
deleted: bool
|
|
|
|
|
|
class CheckDependenciesResponse(CheckDependenciesResult, Hinted):
|
|
pass
|
|
|
|
|
|
class TaskStopResponse(BaseModel):
|
|
"""200 body for POST /apps/<id>/runs/<run_id>:stop and POST /apps/<id>/tasks/<task_id>:stop.
|
|
The handlers always return {"result": "success"}, so `result` is required (no default) — the generated contract
|
|
types it as a required `'success'` rather than an optional field."""
|
|
|
|
result: Literal["success"]
|
|
|
|
|
|
class AppDslImportPayload(BaseModel):
|
|
"""Request body for POST /workspaces/<workspace_id>/apps/imports."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
mode: Literal["yaml-content", "yaml-url"] = Field(..., description="Import mode: yaml-content or yaml-url")
|
|
yaml_content: str | None = Field(None, description="Inline YAML DSL string (required when mode is yaml-content)")
|
|
yaml_url: str | None = Field(None, description="Remote URL to fetch YAML from (required when mode is yaml-url)")
|
|
name: str | None = Field(None, description="Override the app name from the DSL")
|
|
description: str | None = Field(None, description="Override the app description from the DSL")
|
|
icon_type: str | None = Field(None)
|
|
icon: str | None = Field(None)
|
|
icon_background: str | None = Field(None)
|
|
app_id: str | None = Field(None, description="Existing app ID to overwrite (workflow/advanced-chat apps only)")
|
|
draft_hash: str | None = Field(
|
|
None,
|
|
description="draft_hash from the export or restore this import is based on. The import fails if the "
|
|
"draft's graph, features, environment variables or conversation variables changed since. Requires app_id",
|
|
)
|
|
_source: str = PrivateAttr()
|
|
|
|
@model_validator(mode="after")
|
|
def _validate_source_by_mode(self) -> AppDslImportPayload:
|
|
if self.draft_hash is not None and not self.app_id:
|
|
raise ValueError("draft_hash is only valid when app_id names the app to overwrite")
|
|
source = self.yaml_url if self.mode == ImportMode.YAML_URL else self.yaml_content
|
|
if not source:
|
|
raise ValueError(f"{self.mode.replace('-', '_')} is required when mode is '{self.mode}'")
|
|
self._source = source
|
|
return self
|
|
|
|
@property
|
|
def source(self) -> str:
|
|
"""The URL or the YAML text, whichever the mode names."""
|
|
return self._source
|
|
|
|
|
|
class AppDslExportQuery(BaseModel):
|
|
"""Query parameters for GET /apps/<app_id>/dsl."""
|
|
|
|
include_secret: bool = Field(False, description="Include secret values in plaintext in the exported DSL")
|
|
workflow_id: UUIDStr | None = Field(
|
|
None, description="Export a specific workflow version instead of the current draft"
|
|
)
|
|
|
|
|
|
class AppDslExportResponse(BaseModel):
|
|
"""Export DSL response."""
|
|
|
|
data: str = Field(..., description="DSL YAML string")
|
|
draft_hash: str | None = Field(
|
|
None,
|
|
description="Hash of the draft's graph, features, environment variables and conversation variables; "
|
|
"pass it to the import to refuse overwriting newer edits",
|
|
)
|
|
|
|
|
|
class AppDslImportResponse(Import, Hinted):
|
|
"""`Import` plus the server-built next step for a pending import."""
|
|
|
|
|
|
class DslCheckPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
yaml_content: str = Field(description="The DSL to check, as YAML text")
|
|
app_id: str | None = Field(None, description="Check against this existing app's mode, as an overwrite import would")
|
|
|
|
|
|
class DslIssueRow(BaseModel):
|
|
code: str
|
|
severity: str = Field(description="error: import refuses it; warning: import allows it, the release check does not")
|
|
node_id: str | None
|
|
loc: list[str | int]
|
|
message: str
|
|
|
|
@classmethod
|
|
def of(cls, issue: GraphIssue) -> DslIssueRow:
|
|
return cls(
|
|
code=issue.code,
|
|
severity=issue.code.severity,
|
|
node_id=issue.node_id,
|
|
loc=list(issue.loc),
|
|
message=issue.message,
|
|
)
|
|
|
|
|
|
class DslCheckResponse(Hinted):
|
|
valid: bool = Field(description="No error-severity issues")
|
|
issues: list[DslIssueRow]
|
|
|
|
|
|
class ReleaseCheckName(StrEnum):
|
|
DRAFT_VALID = "draft_valid"
|
|
TESTED = "tested"
|
|
|
|
|
|
class ReleaseCheckRow(BaseModel):
|
|
name: ReleaseCheckName
|
|
passed: bool
|
|
detail: str
|
|
|
|
|
|
class ReleaseState(BaseModel):
|
|
service_api_enabled: bool
|
|
webapp_enabled: bool
|
|
|
|
|
|
class ReleaseCheckResponse(Hinted):
|
|
ready: bool = Field(description="Every check passed; show the human `changes` and ask before publishing")
|
|
checks: list[ReleaseCheckRow]
|
|
issues: list[DslIssueRow]
|
|
state: ReleaseState
|
|
changes: WorkflowDiff
|
|
|
|
|
|
class FormSubmitResponse(Hinted):
|
|
"""200 body for POST /apps/<id>/human-input-forms/<token>:submit: the hint to follow the
|
|
resumed run. `extra='forbid'` pins `additionalProperties: false` so the generated contract
|
|
is exact rather than an under-annotated open object."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class OpenApiFormSubmitPayload(HumanInputFormSubmitPayload):
|
|
"""The console payload plus local file parts; `_files.merge_files` sets them on `inputs`."""
|
|
|
|
files: UploadParts | None = Field(
|
|
default=None,
|
|
description="Local file paths keyed by the form's file input name, same convention as the run ops' `files`",
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
class RunListQuery(BaseModel):
|
|
last_id: UUIDStr | None = Field(None, description="Cursor: id of the last run on the previous page")
|
|
limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT)
|
|
status: (
|
|
Literal[
|
|
WorkflowExecutionStatus.RUNNING,
|
|
WorkflowExecutionStatus.SUCCEEDED,
|
|
WorkflowExecutionStatus.FAILED,
|
|
WorkflowExecutionStatus.STOPPED,
|
|
WorkflowExecutionStatus.PARTIAL_SUCCEEDED,
|
|
]
|
|
| None
|
|
) = None
|
|
triggered_from: Literal[WorkflowRunTriggeredFrom.DEBUGGING, WorkflowRunTriggeredFrom.APP_RUN] = Field(
|
|
WorkflowRunTriggeredFrom.DEBUGGING,
|
|
description="debugging: draft test runs, as in the console; app-run: real use",
|
|
)
|
|
|
|
|
|
class RunListResponse(WorkflowRunPaginationResponse, Hinted):
|
|
"""Cursor page of runs; `hints` carries the next page."""
|
|
|
|
|
|
class PublishPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
marked_name: str = Field("", max_length=20, description="Version name")
|
|
marked_comment: str = Field("", max_length=100, description="Version note")
|
|
|
|
|
|
class PublishResponse(BaseModel):
|
|
version_id: str
|
|
created_at: int
|
|
warning: str | None = Field(None, description="Variable references that may read a skipped branch")
|
|
|
|
|
|
class VersionListQuery(PageQuery):
|
|
named_only: bool = Field(False, description="Only versions that have a name")
|
|
|
|
|
|
class VersionRow(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: str
|
|
marked_name: str
|
|
marked_comment: str
|
|
created_by: str | None = None
|
|
created_at: int
|
|
current: bool = False
|
|
|
|
@field_validator("created_at", mode="before")
|
|
@classmethod
|
|
def _timestamp(cls, value: datetime | int) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
|
|
class VersionListResponse(Hinted):
|
|
"""Page of published versions, newest first; there is no total, `hints` carries the next page."""
|
|
|
|
page: int
|
|
limit: int
|
|
has_more: bool
|
|
data: list[VersionRow]
|
|
|
|
|
|
class EnvVariableRow(BaseModel):
|
|
id: str = Field(description="What set and delete take to address this variable")
|
|
name: str = Field(description="What nodes use to refer to this variable")
|
|
description: str = ""
|
|
value_type: str
|
|
value: Any = Field(description="The value; a secret with a value is masked, an empty one reads as empty")
|
|
|
|
|
|
class EnvVariableListResponse(BaseModel):
|
|
data: list[EnvVariableRow]
|
|
|
|
|
|
class EnvVariableValueType(StrEnum):
|
|
"""Value types the draft environment-variable ``set`` op accepts.
|
|
|
|
A curated subset of ``SegmentType``: what the console's environment-variable editor
|
|
allows (``ENVIRONMENT_VARIABLE_SUPPORTED_TYPES`` in controllers/console/app/workflow.py).
|
|
Members reference ``SegmentType.*.value`` so the subset relationship is type-checked.
|
|
"""
|
|
|
|
STRING = SegmentType.STRING.value
|
|
NUMBER = SegmentType.NUMBER.value
|
|
SECRET = SegmentType.SECRET.value
|
|
|
|
|
|
class EnvVariableSetPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str = Field(description="Variable name")
|
|
value_type: EnvVariableValueType = Field(description="string, number or secret")
|
|
value: Any = Field(description="The value; sending the masked value of an existing secret keeps the stored one")
|
|
description: str = Field("", description="What the variable is for")
|
|
|
|
|
|
class RestoreResponse(BaseModel):
|
|
result: Literal["success"]
|
|
draft_hash: str = Field(
|
|
description="Hash of the restored draft's graph, features, environment variables and conversation "
|
|
"variables; pass it to a DSL import as draft_hash"
|
|
)
|
|
|
|
|
|
class NodeTypeRow(BaseModel):
|
|
type: str
|
|
version: str
|
|
|
|
|
|
class NodeTypeListResponse(BaseModel):
|
|
data: list[NodeTypeRow]
|
|
|
|
|
|
class NodeTypeDetailResponse(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
type: str
|
|
version: str
|
|
schema_: dict[str, Any] = Field(alias="schema")
|
|
default_config: dict[str, Any]
|
|
|
|
|
|
AppIconType = Literal[IconType.EMOJI, IconType.IMAGE, IconType.LINK]
|
|
|
|
|
|
class CreateAppPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str = Field(min_length=1, description="App name")
|
|
description: str | None = Field(default=None, max_length=400, description="App description")
|
|
icon_type: AppIconType | None = Field(default=None, description="emoji, image or link")
|
|
icon: str | None = Field(default=None, description="Emoji, file id or URL, per icon_type")
|
|
icon_background: str | None = Field(default=None, description="Background colour for an emoji icon")
|
|
|
|
|
|
class CreatedAppResponse(BaseModel):
|
|
app_id: str
|
|
mode: str
|
|
name: str
|
|
|
|
|
|
class AppSettingsInfo(BaseModel):
|
|
name: str
|
|
description: str | None = None
|
|
icon_type: str | None = None
|
|
icon: str | None = None
|
|
icon_background: str | None = None
|
|
max_active_requests: int | None = None
|
|
|
|
|
|
class ChatAppInfo(AppSettingsInfo):
|
|
use_icon_as_answer_icon: bool = False
|
|
|
|
|
|
class AgentAppInfo(ChatAppInfo):
|
|
role: str | None = None
|
|
|
|
|
|
class AppInfoPatch(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str | None = Field(default=None, min_length=1, description="App name")
|
|
description: str | None = Field(default=None, max_length=400, description="Pass an empty string to clear")
|
|
icon_type: AppIconType | None = Field(default=None, description="emoji, image or link")
|
|
icon: str | None = Field(default=None, description="Emoji, file id or URL, per icon_type")
|
|
icon_background: str | None = Field(default=None, description="Background colour for an emoji icon")
|
|
max_active_requests: int | None = Field(
|
|
default=None,
|
|
ge=0,
|
|
description="Concurrent run cap; 0 uses the server default. The server-wide cap still applies",
|
|
)
|
|
|
|
|
|
class ChatAppInfoPatch(AppInfoPatch):
|
|
use_icon_as_answer_icon: bool | None = Field(default=None, description="Show the app icon on answers")
|
|
|
|
|
|
class AgentAppInfoPatch(ChatAppInfoPatch):
|
|
role: str | None = Field(default=None, max_length=255, description="The agent's role; empty string clears it")
|
|
|
|
|
|
class ServiceApi(BaseModel):
|
|
enabled: bool
|
|
base_url: str
|
|
|
|
|
|
class AgentServiceApi(ServiceApi):
|
|
access_ready: bool
|
|
api_rpm: int = 0
|
|
api_rph: int = 0
|
|
|
|
|
|
class ServiceApiPatch(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
enabled: bool = Field(description="Turn the app's Service API on or off")
|
|
|
|
|
|
class NodeRunPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
inputs: dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Overrides for what the last draft run saved, keyed by variable reference such as #llm.text#",
|
|
)
|
|
files: UploadParts | None = Field(
|
|
default=None,
|
|
description=(
|
|
"Local file paths keyed by the file variable name (a start variable or #node.var#); each is uploaded"
|
|
),
|
|
)
|
|
|
|
|
|
class AdvancedChatNodeRunPayload(NodeRunPayload):
|
|
query: str = Field(default="", description="The user message the node sees as sys.query")
|
|
|
|
|
|
class WebApp(BaseModel):
|
|
enabled: bool
|
|
access_token: str | None = None
|
|
app_base_url: str
|
|
url: str | None = None
|
|
title: str | None = None
|
|
description: str | None = None
|
|
icon_type: str | None = None
|
|
icon: str | None = None
|
|
icon_background: str | None = None
|
|
default_language: str | None = None
|
|
copyright: str | None = None
|
|
privacy_policy: str | None = None
|
|
custom_disclaimer: str | None = None
|
|
|
|
|
|
class WorkflowWebApp(WebApp):
|
|
show_workflow_steps: bool = False
|
|
|
|
|
|
class ChatWebApp(WebApp):
|
|
chat_color_theme: str | None = None
|
|
chat_color_theme_inverted: bool = False
|
|
use_icon_as_answer_icon: bool = False
|
|
input_placeholder: str | None = None
|
|
|
|
|
|
class AdvancedChatWebApp(ChatWebApp):
|
|
show_workflow_steps: bool = False
|
|
|
|
|
|
class AgentWebApp(ChatWebApp):
|
|
access_ready: bool
|
|
|
|
|
|
class WebAppPatch(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
enabled: bool | None = Field(default=None, description="Turn the web app on or off")
|
|
title: str | None = Field(default=None, description="Page title")
|
|
description: str | None = Field(default=None, description="Page description")
|
|
icon_type: AppIconType | None = Field(default=None, description="emoji, image or link")
|
|
icon: str | None = Field(default=None, description="Emoji, file id or URL, per icon_type")
|
|
icon_background: str | None = Field(default=None, description="Background colour for an emoji icon")
|
|
default_language: str | None = Field(default=None, description="Language code, e.g. en-US")
|
|
copyright: str | None = Field(default=None, description="Footer copyright text")
|
|
privacy_policy: str | None = Field(default=None, description="Privacy policy URL")
|
|
custom_disclaimer: str | None = Field(default=None, description="Disclaimer shown on the page")
|
|
|
|
@field_validator("default_language")
|
|
@classmethod
|
|
def _language(cls, value: str | None) -> str | None:
|
|
return value if value is None else supported_language(value)
|
|
|
|
|
|
class WorkflowWebAppPatch(WebAppPatch):
|
|
show_workflow_steps: bool | None = Field(default=None, description="Show each node step to users")
|
|
|
|
|
|
class ChatWebAppPatch(WebAppPatch):
|
|
chat_color_theme: str | None = Field(default=None, description="Chat colour, e.g. #1C64F2")
|
|
chat_color_theme_inverted: bool | None = Field(default=None, description="Invert the chat colours")
|
|
use_icon_as_answer_icon: bool | None = Field(default=None, description="Show the app icon on answers")
|
|
input_placeholder: str | None = Field(default=None, description="Placeholder of the chat input")
|
|
|
|
|
|
class AdvancedChatWebAppPatch(ChatWebAppPatch):
|
|
show_workflow_steps: bool | None = Field(default=None, description="Show each node step to users")
|
|
|
|
|
|
class WebAppToken(BaseModel):
|
|
access_token: str | None = None
|
|
app_base_url: str
|
|
url: str | None = None
|
|
|
|
|
|
class WebAppAccess(BaseModel):
|
|
access_mode: str
|
|
|
|
|
|
class WebAppAccessPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
access_mode: WebAppAccessMode = Field(
|
|
description="public, private_all or sso_verified. Choose specific members (private) in the Dify console"
|
|
)
|
|
|
|
|
|
class _FormOption(Protocol):
|
|
value: str
|
|
|
|
|
|
class _FormShowOn(Protocol):
|
|
variable: str
|
|
value: str
|
|
|
|
|
|
class CredentialFormField(BaseModel):
|
|
name: str
|
|
type: str
|
|
required: bool
|
|
label: str | None
|
|
placeholder: str | None
|
|
options: list[str] | None = Field(description="Allowed values, when the field is a choice")
|
|
show_on: list[dict[str, str]] = Field(description="Show this field only when these other fields have these values")
|
|
|
|
@classmethod
|
|
def of(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
type: object,
|
|
required: bool,
|
|
label: BaseModel | None,
|
|
placeholder: BaseModel | None,
|
|
options: Sequence[_FormOption] | None,
|
|
show_on: Sequence[_FormShowOn] = (),
|
|
language: str | None,
|
|
) -> CredentialFormField:
|
|
"""A model or tool provider's credential form field, cut to what a caller needs."""
|
|
return cls(
|
|
name=name,
|
|
type=str(type),
|
|
required=required,
|
|
label=localized(label, language),
|
|
placeholder=localized(placeholder, language),
|
|
options=[option.value for option in options] if options else None,
|
|
show_on=[{"variable": rule.variable, "value": rule.value} for rule in show_on],
|
|
)
|
|
|
|
|
|
class CredentialRef(BaseModel):
|
|
id: str
|
|
name: str | None
|
|
|
|
|
|
class ModelProviderRow(BaseModel):
|
|
provider: str = Field(description="Provider id such as langgenius/openai/openai")
|
|
label: str | None
|
|
model_types: list[str]
|
|
configured: bool
|
|
active_credential: CredentialRef | None
|
|
|
|
|
|
class CustomModelRow(BaseModel):
|
|
model: str
|
|
model_type: str
|
|
active_credential: CredentialRef | None
|
|
|
|
|
|
class ModelProviderDetailResponse(ModelProviderRow, Hinted):
|
|
credential_form: list[CredentialFormField]
|
|
credentials: list[CredentialRef]
|
|
custom_model_form: list[CredentialFormField] | None
|
|
custom_models: list[CustomModelRow]
|
|
|
|
|
|
_CREDENTIALS_DESCRIPTION: Final = "Secret values, keyed by the field names in credential_form of the describe op."
|
|
_CREDENTIALS_UPDATE_DESCRIPTION: Final = (
|
|
_CREDENTIALS_DESCRIPTION + " Send [__HIDDEN__] for a secret you keep unchanged."
|
|
)
|
|
|
|
|
|
class ProviderCredentialCreatePayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
credentials: dict[str, Any] = Field(description=_CREDENTIALS_DESCRIPTION)
|
|
name: str | None = Field(None, description="Credential name; the server makes one when absent")
|
|
|
|
|
|
class ProviderCredentialUpdatePayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
credentials: dict[str, Any] = Field(description=_CREDENTIALS_UPDATE_DESCRIPTION)
|
|
name: str | None = None
|
|
|
|
|
|
class CredentialWriteResponse(Hinted):
|
|
id: str
|
|
name: str | None
|
|
active: bool
|
|
|
|
|
|
class ToolProviderRow(BaseModel):
|
|
provider: str = Field(description="Tool provider id such as langgenius/tavily/tavily")
|
|
label: str | None
|
|
configured: bool = Field(description="Ready to use: needs no credential, or the workspace has one")
|
|
credential_types: list[str] = Field(description="api-key can be set here; oauth2 needs the console")
|
|
|
|
|
|
class ToolProviderDetailResponse(ToolProviderRow, Hinted):
|
|
credential_form: list[CredentialFormField] = Field(description="Fields of an api-key credential")
|
|
credentials: list[CredentialRef]
|
|
default_credential: CredentialRef | None
|
|
|
|
|
|
class ToolCredentialCreatePayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
credentials: dict[str, Any] = Field(description=_CREDENTIALS_DESCRIPTION)
|
|
name: str | None = Field(None, max_length=30)
|
|
|
|
|
|
class ToolCredentialUpdatePayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
credentials: dict[str, Any] = Field(description=_CREDENTIALS_UPDATE_DESCRIPTION)
|
|
name: str | None = Field(None, max_length=30)
|
|
|
|
|
|
class ToolSource(StrEnum):
|
|
"""Tool provider types a workflow tool node can call; values match the node's provider_type."""
|
|
|
|
BUILTIN = ToolProviderType.BUILT_IN.value
|
|
WORKFLOW = ToolProviderType.WORKFLOW.value
|
|
API = ToolProviderType.API.value
|
|
MCP = ToolProviderType.MCP.value
|
|
|
|
|
|
class ToolListQuery(PageQuery):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
query: str = Field("", description="Words to match in the tool name or label, or the provider id or label")
|
|
provider_type: ToolSource | None = Field(None, description="Only tools of this provider type")
|
|
provider: str | None = Field(None, description="Only this provider's tools")
|
|
|
|
|
|
class ToolInputValue(BaseModel):
|
|
type: ToolInputType = ToolInputType.CONSTANT
|
|
value: Any = None
|
|
|
|
|
|
class ToolParameterRow(BaseModel):
|
|
name: str
|
|
label: str | None
|
|
type: str
|
|
form: str = Field(description="llm: goes in tool_parameters; form: goes in tool_configurations; schema: fixed")
|
|
required: bool
|
|
default: Any
|
|
options: list[str]
|
|
min: float | None
|
|
max: float | None
|
|
llm_description: str | None
|
|
|
|
|
|
TOOL_NODE_VERSION: Final = "2"
|
|
|
|
|
|
class ToolNodeTemplate(BaseModel):
|
|
"""A tool node's data with every value as {type, value}."""
|
|
|
|
type: str = BuiltinNodeTypes.TOOL
|
|
title: str
|
|
provider_type: str
|
|
provider_id: str
|
|
provider_name: str
|
|
plugin_id: str | None
|
|
plugin_unique_identifier: str | None
|
|
tool_name: str
|
|
tool_label: str
|
|
tool_node_version: str = TOOL_NODE_VERSION
|
|
tool_parameters: dict[str, ToolInputValue]
|
|
tool_configurations: dict[str, ToolInputValue]
|
|
|
|
|
|
class ToolRow(BaseModel):
|
|
provider: str
|
|
provider_type: str
|
|
provider_label: str | None
|
|
configured: bool = Field(
|
|
description=(
|
|
"Ready to use: needs no credential, or the workspace has one. MCP: authorize or refresh in the console"
|
|
)
|
|
)
|
|
name: str
|
|
label: str
|
|
description: str | None
|
|
parameters: list[ToolParameterRow]
|
|
node_data: ToolNodeTemplate = Field(
|
|
description=(
|
|
"Use as the tool node's data; fill every null value; pass upstream values as "
|
|
'{type: mixed, value: "{{#node.var#}}"} or {type: variable, value: [node, var]}'
|
|
)
|
|
)
|
|
|
|
|
|
class ToolListResponse(PaginationEnvelope[ToolRow]):
|
|
pass
|
|
|
|
|
|
class ModelListQuery(PageQuery):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
provider: str | None = Field(None, description="Only this provider's models, such as langgenius/openai/openai")
|
|
model_type: ModelType | None = Field(None, description="Only models of this type")
|
|
query: str = Field("", description="Words to match in the model name, label or provider")
|
|
|
|
|
|
class LlmModelBlock(BaseModel):
|
|
provider: str
|
|
name: str
|
|
mode: str
|
|
completion_params: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class ModelRow(BaseModel):
|
|
provider: str
|
|
provider_label: str | None
|
|
model: str
|
|
model_type: str
|
|
label: str | None
|
|
status: str = Field(description="active means usable now; anything else needs the provider set up")
|
|
features: list[str]
|
|
node_model: LlmModelBlock | None = Field(None, description="LLM rows: paste as the node's model")
|
|
|
|
|
|
class ModelListResponse(PaginationEnvelope[ModelRow]):
|
|
pass
|
|
|
|
|
|
KNOWLEDGE_TOP_K: Final = 4
|
|
|
|
|
|
class MultipleRetrievalFragment(BaseModel):
|
|
top_k: int = KNOWLEDGE_TOP_K
|
|
reranking_enable: bool = False
|
|
|
|
|
|
class KnowledgeRetrievalFragment(BaseModel):
|
|
dataset_ids: list[str]
|
|
retrieval_mode: Literal["multiple"] = "multiple"
|
|
multiple_retrieval_config: MultipleRetrievalFragment = Field(default_factory=MultipleRetrievalFragment)
|
|
|
|
|
|
class KnowledgeBaseListQuery(PageQuery):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
query: str = Field("", description="Words to match in the knowledge base name")
|
|
|
|
|
|
class KnowledgeBaseRow(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: str | None
|
|
provider: str = Field(description="vendor: indexed in Dify; external: an external knowledge API")
|
|
indexing_technique: str | None
|
|
document_count: int
|
|
usable: bool = Field(description="false when its embedding model isn't available in this workspace")
|
|
node_data: KnowledgeRetrievalFragment = Field(
|
|
description=(
|
|
"Merge into a knowledge-retrieval node's data and set query_variable_selector; "
|
|
"to search several bases, join their dataset_ids. For rerank or weights read describe node_type"
|
|
)
|
|
)
|
|
|
|
|
|
class KnowledgeBaseListResponse(PaginationEnvelope[KnowledgeBaseRow]):
|
|
pass
|
|
|
|
|
|
class ModelRef(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
model: str = Field(description="Model name, as in get.model")
|
|
model_type: ModelType
|
|
|
|
|
|
class ModelCredentialCreatePayload(ModelRef):
|
|
credentials: dict[str, Any] = Field(description=_CREDENTIALS_DESCRIPTION)
|
|
name: str | None = None
|
|
|
|
|
|
class ModelCredentialUpdatePayload(ModelRef):
|
|
credentials: dict[str, Any] = Field(description=_CREDENTIALS_UPDATE_DESCRIPTION)
|
|
name: str | None = None
|