441 lines
13 KiB
Python
441 lines
13 KiB
Python
import binascii
|
|
import json
|
|
from collections.abc import Mapping
|
|
from typing import Any, Literal
|
|
|
|
from flask import Response
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
from core.datasource.entities.datasource_entities import (
|
|
DatasourceProviderType,
|
|
GetOnlineDocumentPageContentRequest,
|
|
)
|
|
from core.entities.embedding_type import EmbeddingInputType
|
|
from core.entities.provider_entities import BasicProviderConfig
|
|
from core.plugin.utils.http_parser import deserialize_response
|
|
from core.workflow.file_reference import is_canonical_file_reference
|
|
from graphon.model_runtime.entities.message_entities import (
|
|
AssistantPromptMessage,
|
|
PromptMessage,
|
|
PromptMessageRole,
|
|
PromptMessageTool,
|
|
SystemPromptMessage,
|
|
ToolPromptMessage,
|
|
UserPromptMessage,
|
|
)
|
|
from graphon.model_runtime.entities.model_entities import ModelType
|
|
from graphon.nodes.llm.entities import ModelConfig as LLMModelConfig
|
|
from graphon.nodes.parameter_extractor.entities import (
|
|
ParameterConfig,
|
|
)
|
|
from graphon.nodes.question_classifier.entities import (
|
|
ClassConfig,
|
|
)
|
|
|
|
|
|
class InvokeCredentials(BaseModel):
|
|
tool_credentials: dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Map of tool provider to credential id, used to store the credential id for the tool provider.",
|
|
)
|
|
|
|
|
|
class PluginInvokeContext(BaseModel):
|
|
credentials: InvokeCredentials | None = Field(
|
|
default_factory=InvokeCredentials,
|
|
description="Credentials context for the plugin invocation or backward invocation.",
|
|
)
|
|
|
|
|
|
class RequestInvokeTool(BaseModel):
|
|
"""
|
|
Request to invoke a tool
|
|
"""
|
|
|
|
tool_type: Literal["builtin", "workflow", "api", "mcp"]
|
|
provider: str
|
|
tool: str
|
|
tool_parameters: dict[str, Any]
|
|
credential_id: str | None = None
|
|
|
|
|
|
DatasourceInvocationOperation = Literal[
|
|
"get_online_document_page_content",
|
|
"get_online_document_pages",
|
|
"get_website_crawl",
|
|
"online_drive_browse_files",
|
|
"online_drive_download_file",
|
|
"validate_credentials",
|
|
]
|
|
|
|
|
|
class RequestInvokeDatasource(BaseModel):
|
|
"""Invoke one installed datasource using a Dify-owned credential reference.
|
|
|
|
Raw credentials are intentionally not part of this contract. ``tenant_id`` and
|
|
``user_id`` are consumed by the inner-API request context, while the remaining
|
|
fields select an installed provider declaration and an operation-specific input.
|
|
"""
|
|
|
|
tenant_id: str = Field(min_length=1, max_length=512)
|
|
user_id: str = Field(min_length=1, max_length=512)
|
|
provider: str = Field(min_length=1, max_length=768)
|
|
datasource: str = Field(min_length=1, max_length=256)
|
|
datasource_type: DatasourceProviderType
|
|
credential_id: str = Field(min_length=1, max_length=512)
|
|
operation: DatasourceInvocationOperation
|
|
datasource_parameters: dict[str, Any] = Field(default_factory=dict)
|
|
page: GetOnlineDocumentPageContentRequest | None = None
|
|
request: dict[str, Any] | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
@model_validator(mode="after")
|
|
def validate_operation_payload(self) -> "RequestInvokeDatasource":
|
|
expected_type = {
|
|
"get_online_document_page_content": DatasourceProviderType.ONLINE_DOCUMENT,
|
|
"get_online_document_pages": DatasourceProviderType.ONLINE_DOCUMENT,
|
|
"get_website_crawl": DatasourceProviderType.WEBSITE_CRAWL,
|
|
"online_drive_browse_files": DatasourceProviderType.ONLINE_DRIVE,
|
|
"online_drive_download_file": DatasourceProviderType.ONLINE_DRIVE,
|
|
"validate_credentials": self.datasource_type,
|
|
}[self.operation]
|
|
if self.datasource_type != expected_type:
|
|
raise ValueError(f"{self.operation} requires datasource_type {expected_type.value}")
|
|
|
|
page_required = self.operation == "get_online_document_page_content"
|
|
if page_required != (self.page is not None):
|
|
raise ValueError("page is required only for get_online_document_page_content")
|
|
|
|
request_required = self.operation in {"online_drive_browse_files", "online_drive_download_file"}
|
|
if request_required != (self.request is not None):
|
|
raise ValueError("request is required only for online-drive operations")
|
|
|
|
return self
|
|
|
|
|
|
class BaseRequestInvokeModel(BaseModel):
|
|
provider: str
|
|
model: str
|
|
model_type: ModelType
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
|
|
class RequestInvokeLLM(BaseRequestInvokeModel):
|
|
"""
|
|
Request to invoke LLM
|
|
"""
|
|
|
|
model_type: ModelType = ModelType.LLM
|
|
mode: str
|
|
completion_params: dict[str, Any] = Field(default_factory=dict)
|
|
prompt_messages: list[PromptMessage] = Field(default_factory=list)
|
|
tools: list[PromptMessageTool] | None = Field(default_factory=list[PromptMessageTool])
|
|
stop: list[str] | None = Field(default_factory=list[str])
|
|
stream: bool = False
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
@field_validator("prompt_messages", mode="before")
|
|
@classmethod
|
|
def convert_prompt_messages(cls, v):
|
|
if not isinstance(v, list):
|
|
raise ValueError("prompt_messages must be a list")
|
|
|
|
for i in range(len(v)):
|
|
if v[i]["role"] == PromptMessageRole.USER:
|
|
v[i] = UserPromptMessage.model_validate(v[i])
|
|
elif v[i]["role"] == PromptMessageRole.ASSISTANT:
|
|
v[i] = AssistantPromptMessage.model_validate(v[i])
|
|
elif v[i]["role"] == PromptMessageRole.SYSTEM:
|
|
v[i] = SystemPromptMessage.model_validate(v[i])
|
|
elif v[i]["role"] == PromptMessageRole.TOOL:
|
|
v[i] = ToolPromptMessage.model_validate(v[i])
|
|
else:
|
|
v[i] = PromptMessage.model_validate(v[i])
|
|
|
|
return v
|
|
|
|
|
|
class RequestInvokeLLMWithStructuredOutput(RequestInvokeLLM):
|
|
"""
|
|
Request to invoke LLM with structured output
|
|
"""
|
|
|
|
structured_output_schema: dict[str, Any] = Field(
|
|
default_factory=dict, description="The schema of the structured output in JSON schema format"
|
|
)
|
|
|
|
|
|
class RequestInvokeTextEmbedding(BaseRequestInvokeModel):
|
|
"""
|
|
Request to invoke text embedding
|
|
"""
|
|
|
|
model_type: ModelType = ModelType.TEXT_EMBEDDING
|
|
texts: list[str]
|
|
input_type: EmbeddingInputType = EmbeddingInputType.DOCUMENT
|
|
|
|
|
|
class MultimodalEmbeddingDocument(BaseModel):
|
|
"""A document accepted by a multimodal text-embedding model."""
|
|
|
|
content: str
|
|
content_type: str
|
|
file_id: str | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class RequestInvokeMultimodalEmbedding(BaseRequestInvokeModel):
|
|
"""Request to invoke a multimodal text-embedding model."""
|
|
|
|
model_type: ModelType = ModelType.TEXT_EMBEDDING
|
|
documents: list[MultimodalEmbeddingDocument] = Field(min_length=1)
|
|
input_type: EmbeddingInputType = EmbeddingInputType.DOCUMENT
|
|
|
|
|
|
class RequestInvokeRerank(BaseRequestInvokeModel):
|
|
"""
|
|
Request to invoke rerank
|
|
"""
|
|
|
|
model_type: ModelType = ModelType.RERANK
|
|
query: str
|
|
docs: list[str]
|
|
score_threshold: float | None = None
|
|
top_n: int | None = None
|
|
|
|
|
|
class RequestListModels(BaseModel):
|
|
"""Tenant-scoped query for models that Dify can invoke."""
|
|
|
|
model_type: Literal[ModelType.LLM, ModelType.TEXT_EMBEDDING, ModelType.RERANK]
|
|
provider: str | None = None
|
|
model: str | None = None
|
|
offset: int = Field(default=0, ge=0)
|
|
limit: int = Field(default=50, ge=1, le=100)
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
|
|
class InvokableModelCatalogItem(BaseModel):
|
|
"""Installed identity and active Dify capability metadata for one model."""
|
|
|
|
plugin_id: str
|
|
plugin_unique_identifier: str
|
|
provider: str
|
|
model: str
|
|
model_type: ModelType
|
|
capabilities: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
|
|
class InvokableModelCatalogPage(BaseModel):
|
|
"""Offset page returned by the internal model catalog endpoint."""
|
|
|
|
items: list[InvokableModelCatalogItem] = Field(default_factory=list)
|
|
next_offset: int | None = None
|
|
|
|
|
|
class RequestInvokeTTS(BaseRequestInvokeModel):
|
|
"""
|
|
Request to invoke TTS
|
|
"""
|
|
|
|
model_type: ModelType = ModelType.TTS
|
|
content_text: str
|
|
voice: str
|
|
|
|
|
|
class RequestInvokeSpeech2Text(BaseRequestInvokeModel):
|
|
"""
|
|
Request to invoke speech2text
|
|
"""
|
|
|
|
model_type: ModelType = ModelType.SPEECH2TEXT
|
|
file: bytes
|
|
|
|
@field_validator("file", mode="before")
|
|
@classmethod
|
|
def convert_file(cls, v):
|
|
# hex string to bytes
|
|
if isinstance(v, str):
|
|
return bytes.fromhex(v)
|
|
else:
|
|
raise ValueError("file must be a hex string")
|
|
|
|
|
|
class RequestInvokeModeration(BaseRequestInvokeModel):
|
|
"""
|
|
Request to invoke moderation
|
|
"""
|
|
|
|
model_type: ModelType = ModelType.MODERATION
|
|
text: str
|
|
|
|
|
|
class RequestInvokeParameterExtractorNode(BaseModel):
|
|
"""
|
|
Request to invoke parameter extractor node
|
|
"""
|
|
|
|
parameters: list[ParameterConfig]
|
|
model: LLMModelConfig
|
|
instruction: str
|
|
query: str
|
|
|
|
|
|
class RequestInvokeQuestionClassifierNode(BaseModel):
|
|
"""
|
|
Request to invoke question classifier node
|
|
"""
|
|
|
|
query: str
|
|
model: LLMModelConfig
|
|
classes: list[ClassConfig]
|
|
instruction: str
|
|
|
|
|
|
class RequestInvokeApp(BaseModel):
|
|
"""
|
|
Request to invoke app
|
|
"""
|
|
|
|
app_id: str
|
|
inputs: dict[str, Any]
|
|
query: str | None = None
|
|
response_mode: Literal["blocking", "streaming"]
|
|
conversation_id: str | None = None
|
|
user: str | None = None
|
|
files: list[dict] = Field(default_factory=list)
|
|
|
|
|
|
class RequestInvokeEncrypt(BaseModel):
|
|
"""
|
|
Request to encryption
|
|
"""
|
|
|
|
opt: Literal["encrypt", "decrypt", "clear"]
|
|
namespace: Literal["endpoint"]
|
|
identity: str
|
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
config: list[BasicProviderConfig] = Field(default_factory=list)
|
|
|
|
|
|
class RequestInvokeSummary(BaseModel):
|
|
"""
|
|
Request to summary
|
|
"""
|
|
|
|
text: str
|
|
instruction: str
|
|
|
|
|
|
class RequestRequestUploadFile(BaseModel):
|
|
"""
|
|
Request to upload file
|
|
"""
|
|
|
|
filename: str
|
|
mimetype: str
|
|
conversation_id: str | None = None
|
|
|
|
|
|
class RequestDownloadFileMapping(BaseModel):
|
|
"""File mapping accepted by trusted download-request control-plane APIs."""
|
|
|
|
transfer_method: Literal["local_file", "tool_file", "datasource_file", "remote_url"]
|
|
reference: str | None = None
|
|
url: str | None = None
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
@model_validator(mode="after")
|
|
def validate_locator(self) -> "RequestDownloadFileMapping":
|
|
if self.transfer_method == "remote_url":
|
|
if not self.url:
|
|
raise ValueError("url is required when transfer_method is remote_url")
|
|
if self.reference is not None:
|
|
raise ValueError("reference is not allowed when transfer_method is remote_url")
|
|
return self
|
|
if not self.reference:
|
|
raise ValueError("reference is required for non-remote file mappings")
|
|
if not is_canonical_file_reference(self.reference):
|
|
raise ValueError("reference must be a canonical Dify file reference")
|
|
if self.url is not None:
|
|
raise ValueError("url is not allowed for non-remote file mappings")
|
|
return self
|
|
|
|
|
|
class RequestRequestDownloadFile(BaseModel):
|
|
"""Request to resolve a signed download URL for one runtime file mapping."""
|
|
|
|
tenant_id: str
|
|
user_id: str
|
|
user_from: Literal["account", "end-user"]
|
|
invoke_from: Literal[
|
|
"service-api",
|
|
"openapi",
|
|
"web-app",
|
|
"trigger",
|
|
"explore",
|
|
"debugger",
|
|
"published",
|
|
"validation",
|
|
]
|
|
file: RequestDownloadFileMapping
|
|
for_external: bool = True
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class RequestFetchAppInfo(BaseModel):
|
|
"""
|
|
Request to fetch app info
|
|
"""
|
|
|
|
app_id: str
|
|
|
|
|
|
class TriggerInvokeEventResponse(BaseModel):
|
|
variables: Mapping[str, Any] = Field(default_factory=dict)
|
|
cancelled: bool = Field(default=False)
|
|
|
|
model_config = ConfigDict(protected_namespaces=(), arbitrary_types_allowed=True)
|
|
|
|
@field_validator("variables", mode="before")
|
|
@classmethod
|
|
def convert_variables(cls, v):
|
|
if isinstance(v, str):
|
|
return json.loads(v)
|
|
else:
|
|
return v
|
|
|
|
|
|
class TriggerSubscriptionResponse(BaseModel):
|
|
subscription: dict[str, Any]
|
|
|
|
|
|
class TriggerValidateProviderCredentialsResponse(BaseModel):
|
|
result: bool
|
|
|
|
|
|
class TriggerDispatchResponse(BaseModel):
|
|
user_id: str
|
|
events: list[str]
|
|
response: Response
|
|
payload: Mapping[str, Any] = Field(default_factory=dict)
|
|
|
|
model_config = ConfigDict(protected_namespaces=(), arbitrary_types_allowed=True)
|
|
|
|
@field_validator("response", mode="before")
|
|
@classmethod
|
|
def convert_response(cls, v: str):
|
|
try:
|
|
return deserialize_response(binascii.unhexlify(v.encode()))
|
|
except Exception as e:
|
|
raise ValueError("Failed to deserialize response from hex string") from e
|