Files

251 lines
9.3 KiB
Python

"""Agent-owned inner endpoints for CLI file URL allocation."""
from __future__ import annotations
from typing import Literal
from dify_agent.adapters.native_testing.client import AgentPhysicalDispatchPayload
from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from sqlalchemy.orm import Session
from configs import dify_config
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session
from controllers.console.wraps import setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.plugin.wraps import get_user
from controllers.inner_api.wraps import agent_endpoint_api_only
from core.plugin.entities.request import RequestDownloadFileMapping, RequestRequestUploadFile
from core.tools.signature import bind_file_uri, get_signed_file_uri_for_plugin
from extensions.ext_application_services import application_services
from fields.base import ResponseModel
from libs.exception import BaseHTTPException
from runtime_contracts.builder.execution import BuilderExecutionPolicyError
from services.dify_builder.native_testing.agent_bridge import claim_agent_endpoint
from services.file_request_service import FileRequestService
class AgentFileRequestHttpError(BaseHTTPException):
error_code = "agent_file_request_failed"
description = "Agent file request failed."
code = 500
def __init__(self, *, error_code: str, description: str, status_code: int) -> None:
self.error_code = error_code
self.description = description
self.code = status_code
super().__init__(description)
class AgentFileUploadRequestPayload(RequestRequestUploadFile):
tenant_id: str
user_id: str
user_from: Literal["account", "end-user"] | None = None
max_size: int = Field(ge=0, description="Maximum upload size in bytes")
native_dispatch: AgentPhysicalDispatchPayload | None = None
model_config = ConfigDict(extra="forbid")
class AgentFileDownloadRequestPayload(BaseModel):
native_dispatch: AgentPhysicalDispatchPayload | None = None
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_frontend: bool = True
for_external: bool = True
model_config = ConfigDict(extra="forbid")
class AgentFileUploadRequestResponse(ResponseModel):
upload_uri: str
class AgentFileDownloadRequestResponse(ResponseModel):
filename: str
mime_type: str | None = None
size: int
download_uri: str
register_schema_models(inner_api_ns, AgentFileUploadRequestPayload, AgentFileDownloadRequestPayload)
register_response_schema_models(
inner_api_ns,
AgentFileUploadRequestResponse,
AgentFileDownloadRequestResponse,
)
@inner_api_ns.route("/agent/files/upload-request")
class AgentFileUploadRequestApi(Resource):
"""Allocate an origin-free signed upload URI for the Agent CLI."""
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("inner_agent_file_upload_request")
@inner_api_ns.expect(inner_api_ns.models[AgentFileUploadRequestPayload.__name__])
@inner_api_ns.response(
200,
"Upload URI allocated",
inner_api_ns.models[AgentFileUploadRequestResponse.__name__],
)
@with_session(write=False)
def post(self, session: Session) -> dict[str, object]:
try:
payload = AgentFileUploadRequestPayload.model_validate(inner_api_ns.payload or {})
except ValidationError as exc:
raise AgentFileRequestHttpError(
error_code="invalid_request",
description=str(exc),
status_code=400,
) from exc
try:
claim_agent_endpoint(
boundary="file_upload",
caller={"tenant_id": payload.tenant_id, "user_id": payload.user_id},
envelope=payload.native_dispatch,
target={"direction": "upload"},
parameters=payload.model_dump(mode="json", exclude={"native_dispatch"}),
kind="file",
physical_owner_ready=False,
)
except BuilderExecutionPolicyError:
raise AgentFileRequestHttpError(
error_code="native_agent_control_refused", description="Native Agent dispatch refused.", status_code=409
) from None
tenant = application_services().workspaces.management.get(payload.tenant_id)
if tenant is None:
raise AgentFileRequestHttpError(
error_code="tenant_not_found",
description="tenant not found",
status_code=404,
)
try:
if payload.user_from == "account":
if not application_services().workspaces.members.get_role(tenant.id, payload.user_id):
raise ValueError("account not found")
owner_id = payload.user_id
else:
owner_id = get_user(tenant.id, payload.user_id).id
upload_uri = get_signed_file_uri_for_plugin(
filename=payload.filename,
mimetype=payload.mimetype,
tenant_id=tenant.id,
user_id=owner_id,
conversation_id=payload.conversation_id,
user_from=payload.user_from,
max_size=payload.max_size,
)
except ValueError as exc:
raise AgentFileRequestHttpError(
error_code="user_not_found",
description=str(exc),
status_code=404,
) from exc
return AgentFileUploadRequestResponse(upload_uri=upload_uri).model_dump(mode="json")
@inner_api_ns.route("/agent/files/download-request")
class AgentFileDownloadRequestApi(Resource):
"""Allocate a transfer URI or a signed URL for one Agent file."""
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("inner_agent_file_download_request")
@inner_api_ns.expect(inner_api_ns.models[AgentFileDownloadRequestPayload.__name__])
@inner_api_ns.response(
200,
"Download URI allocated",
inner_api_ns.models[AgentFileDownloadRequestResponse.__name__],
)
@with_session(write=False)
def post(self, session: Session) -> dict[str, object]:
"""Resolve an authorized file and bind its signed URI when requested.
Transfer consumers receive the resolved URI directly. Bound URLs use
the external origin by default. Plugin consumers can request the
internal origin, with the external origin as a fallback.
"""
try:
payload = AgentFileDownloadRequestPayload.model_validate(inner_api_ns.payload or {})
except ValidationError as exc:
raise AgentFileRequestHttpError(
error_code="invalid_request",
description=str(exc),
status_code=400,
) from exc
try:
claim_agent_endpoint(
boundary="file_download",
caller={"tenant_id": payload.tenant_id, "user_id": payload.user_id},
envelope=payload.native_dispatch,
target={"direction": "download"},
parameters=payload.model_dump(mode="json", exclude={"native_dispatch"}),
kind="file",
physical_owner_ready=False,
)
except BuilderExecutionPolicyError:
raise AgentFileRequestHttpError(
error_code="native_agent_control_refused", description="Native Agent dispatch refused.", status_code=409
) from None
if application_services().workspaces.management.get(payload.tenant_id) is None:
raise AgentFileRequestHttpError(
error_code="tenant_not_found",
description="tenant not found",
status_code=404,
)
try:
result = FileRequestService().request_download(
tenant_id=payload.tenant_id,
user_id=payload.user_id,
user_from=payload.user_from,
invoke_from=payload.invoke_from,
file_mapping=payload.file.model_dump(mode="python", exclude_none=True),
)
except ValueError as exc:
raise AgentFileRequestHttpError(
error_code="file_not_accessible",
description=str(exc),
status_code=404,
) from exc
download_uri = result.download_uri
if payload.for_frontend:
base_url = dify_config.FILES_URL
if not payload.for_external:
base_url = dify_config.INTERNAL_FILES_URL or base_url
download_uri = bind_file_uri(download_uri, base_url)
return AgentFileDownloadRequestResponse(
filename=result.filename,
mime_type=result.mime_type,
size=result.size,
download_uri=download_uri,
).model_dump(mode="json")
__all__ = [
"AgentFileDownloadRequestApi",
"AgentFileDownloadRequestPayload",
"AgentFileDownloadRequestResponse",
"AgentFileUploadRequestApi",
"AgentFileUploadRequestPayload",
"AgentFileUploadRequestResponse",
]