mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
289 lines
14 KiB
Python
289 lines
14 KiB
Python
from http import HTTPStatus
|
|
from typing import Literal
|
|
from uuid import UUID
|
|
|
|
from flask_restx import Resource
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from controllers.common.errors import ConversationNotFoundHTTPError, InvalidArgumentError, UnauthorizedError
|
|
from controllers.common.rbac import PlainApp, RBACCheck
|
|
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 AppNotFoundError
|
|
from controllers.console.flask_admission import console_account_admission
|
|
from controllers.console.wraps import RBACPermission, validate_request
|
|
from enums.account import TenantAccountRole
|
|
from extensions.ext_application_services import application_services
|
|
from fields.conversation_fields import (
|
|
Conversation as ConversationResponse,
|
|
)
|
|
from fields.conversation_fields import (
|
|
ConversationDetail as ConversationDetailResponse,
|
|
)
|
|
from fields.conversation_fields import (
|
|
ConversationMessageDetail as ConversationMessageDetailResponse,
|
|
)
|
|
from fields.conversation_fields import (
|
|
ConversationPagination as ConversationPaginationResponse,
|
|
)
|
|
from fields.conversation_fields import (
|
|
ConversationWithSummaryPagination as ConversationWithSummaryPaginationResponse,
|
|
)
|
|
from libs.datetime_utils import parse_time_range
|
|
from libs.external_api import error_responses
|
|
from libs.helper import dump_response
|
|
from machinery.context import RequestContext
|
|
from services.app.console_service import ConsoleAppNotFoundError
|
|
from services.entities.conversation_entities import ConsoleConversationFilters
|
|
from services.errors.base import NoPermissionError
|
|
from services.errors.conversation import ConversationNotExistsError
|
|
|
|
|
|
class BaseConversationQuery(BaseModel):
|
|
keyword: str | None = Field(default=None, description="Search keyword")
|
|
start: str | None = Field(default=None, description="Start date (YYYY-MM-DD HH:MM)")
|
|
end: str | None = Field(default=None, description="End date (YYYY-MM-DD HH:MM)")
|
|
annotation_status: Literal["annotated", "not_annotated", "all"] = Field(
|
|
default="all", description="Annotation status filter"
|
|
)
|
|
page: int = Field(default=1, ge=1, le=99999, description="Page number")
|
|
limit: int = Field(default=20, ge=1, le=100, description="Page size (1-100)")
|
|
|
|
@field_validator("start", "end", mode="before")
|
|
@classmethod
|
|
def blank_to_none(cls, value: str | None) -> str | None:
|
|
if value == "":
|
|
return None
|
|
return value
|
|
|
|
|
|
class CompletionConversationQuery(BaseConversationQuery):
|
|
pass
|
|
|
|
|
|
class ChatConversationQuery(BaseConversationQuery):
|
|
sort_by: Literal["created_at", "-created_at", "updated_at", "-updated_at"] = Field(
|
|
default="-updated_at", description="Sort field and direction"
|
|
)
|
|
|
|
|
|
register_schema_models(
|
|
console_ns,
|
|
CompletionConversationQuery,
|
|
ChatConversationQuery,
|
|
)
|
|
register_response_schema_models(
|
|
console_ns,
|
|
ConversationResponse,
|
|
ConversationPaginationResponse,
|
|
ConversationMessageDetailResponse,
|
|
ConversationWithSummaryPaginationResponse,
|
|
ConversationDetailResponse,
|
|
)
|
|
|
|
|
|
@console_ns.route("/apps/<uuid:app_id>/completion-conversations")
|
|
class CompletionConversationApi(Resource):
|
|
@error_responses(AppNotFoundError)
|
|
@console_ns.doc("list_completion_conversations")
|
|
@console_ns.doc(description="Get completion conversations with pagination and filtering")
|
|
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(CompletionConversationQuery)})
|
|
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[ConversationPaginationResponse.__name__])
|
|
@console_ns.response(HTTPStatus.BAD_REQUEST, "Invalid date range")
|
|
@console_ns.response(HTTPStatus.UNAUTHORIZED, "Authentication required")
|
|
@console_ns.response(HTTPStatus.FORBIDDEN, "Insufficient permissions")
|
|
@console_ns.response(HTTPStatus.NOT_FOUND, "App not found")
|
|
@console_ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, "Invalid query parameters")
|
|
@console_account_admission(
|
|
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
|
|
rbac_checks=(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()),),
|
|
)
|
|
def get(self, request_context: RequestContext, app_id: UUID) -> dict[str, object]:
|
|
queries = application_services().conversation_queries
|
|
try:
|
|
scope = queries.get_console_scope(request_context, str(app_id), kind="completion")
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
except NoPermissionError as exc:
|
|
raise UnauthorizedError(str(exc)) from exc
|
|
req_data = validate_request(CompletionConversationQuery)
|
|
try:
|
|
start, end = parse_time_range(req_data.start, req_data.end, scope.timezone)
|
|
except ValueError as exc:
|
|
raise InvalidArgumentError(str(exc)) from exc
|
|
try:
|
|
page = queries.get_console_page(
|
|
scope=scope,
|
|
filters=ConsoleConversationFilters(
|
|
keyword=req_data.keyword,
|
|
start=start,
|
|
end=end.replace(second=59) if end else None,
|
|
annotation_status=req_data.annotation_status,
|
|
sort_by="-created_at",
|
|
page=req_data.page,
|
|
limit=req_data.limit,
|
|
),
|
|
)
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
return dump_response(ConversationPaginationResponse, page)
|
|
|
|
|
|
@console_ns.route("/apps/<uuid:app_id>/completion-conversations/<uuid:conversation_id>")
|
|
class CompletionConversationDetailApi(Resource):
|
|
@error_responses(AppNotFoundError, ConversationNotFoundHTTPError)
|
|
@console_ns.doc("get_completion_conversation")
|
|
@console_ns.doc(description="Get completion conversation details with messages")
|
|
@console_ns.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
|
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[ConversationMessageDetailResponse.__name__])
|
|
@console_ns.response(HTTPStatus.UNAUTHORIZED, "Authentication required")
|
|
@console_ns.response(HTTPStatus.FORBIDDEN, "Insufficient permissions")
|
|
@console_ns.response(HTTPStatus.NOT_FOUND, "App or conversation not found")
|
|
@console_account_admission(
|
|
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
|
|
rbac_checks=(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()),),
|
|
)
|
|
def get(self, request_context: RequestContext, app_id: UUID, conversation_id: UUID) -> dict[str, object]:
|
|
services = application_services()
|
|
try:
|
|
detail = services.conversation_queries.get_console_detail(
|
|
request_context, str(app_id), str(conversation_id), kind="completion"
|
|
)
|
|
response = dump_response(ConversationMessageDetailResponse, detail)
|
|
# Preserve the Console read receipt only after rendering succeeds.
|
|
services.conversation_commands.mark_console_read(
|
|
request_context, str(app_id), str(conversation_id), kind="completion"
|
|
)
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
except ConversationNotExistsError as exc:
|
|
raise ConversationNotFoundHTTPError() from exc
|
|
return response
|
|
|
|
@error_responses(AppNotFoundError, ConversationNotFoundHTTPError)
|
|
@console_ns.doc("delete_completion_conversation")
|
|
@console_ns.doc(description="Delete a completion conversation")
|
|
@console_ns.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
|
@console_ns.response(HTTPStatus.NO_CONTENT, "Conversation deleted successfully")
|
|
@console_ns.response(HTTPStatus.UNAUTHORIZED, "Authentication required")
|
|
@console_ns.response(HTTPStatus.FORBIDDEN, "Insufficient permissions")
|
|
@console_ns.response(HTTPStatus.NOT_FOUND, "App or conversation not found")
|
|
@console_account_admission(
|
|
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
|
|
rbac_checks=(RBACCheck(RBACPermission.APP_EDIT, PlainApp()),),
|
|
)
|
|
def delete(self, request_context: RequestContext, app_id: UUID, conversation_id: UUID) -> tuple[str, HTTPStatus]:
|
|
try:
|
|
application_services().conversation_commands.delete_console(
|
|
request_context, str(app_id), str(conversation_id), kind="completion"
|
|
)
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
except ConversationNotExistsError as exc:
|
|
raise ConversationNotFoundHTTPError() from exc
|
|
return "", HTTPStatus.NO_CONTENT
|
|
|
|
|
|
@console_ns.route("/apps/<uuid:app_id>/chat-conversations")
|
|
class ChatConversationApi(Resource):
|
|
@error_responses(AppNotFoundError)
|
|
@console_ns.doc("list_chat_conversations")
|
|
@console_ns.doc(description="Get chat conversations with pagination and filtering")
|
|
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(ChatConversationQuery)})
|
|
@console_ns.response(
|
|
HTTPStatus.OK, "Success", console_ns.models[ConversationWithSummaryPaginationResponse.__name__]
|
|
)
|
|
@console_ns.response(HTTPStatus.BAD_REQUEST, "Invalid date range")
|
|
@console_ns.response(HTTPStatus.UNAUTHORIZED, "Authentication required")
|
|
@console_ns.response(HTTPStatus.FORBIDDEN, "Insufficient permissions")
|
|
@console_ns.response(HTTPStatus.NOT_FOUND, "App not found")
|
|
@console_ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, "Invalid query parameters")
|
|
@console_account_admission(
|
|
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
|
|
rbac_checks=(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()),),
|
|
)
|
|
def get(self, request_context: RequestContext, app_id: UUID) -> dict[str, object]:
|
|
queries = application_services().conversation_queries
|
|
try:
|
|
scope = queries.get_console_scope(request_context, str(app_id), kind="chat")
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
except NoPermissionError as exc:
|
|
raise UnauthorizedError(str(exc)) from exc
|
|
req_data = validate_request(ChatConversationQuery)
|
|
try:
|
|
start, end = parse_time_range(req_data.start, req_data.end, scope.timezone)
|
|
except ValueError as exc:
|
|
raise InvalidArgumentError(str(exc)) from exc
|
|
try:
|
|
page = queries.get_console_page(
|
|
scope=scope,
|
|
filters=ConsoleConversationFilters(
|
|
keyword=req_data.keyword,
|
|
start=start,
|
|
end=end.replace(second=59) if end else None,
|
|
annotation_status=req_data.annotation_status,
|
|
sort_by=req_data.sort_by,
|
|
page=req_data.page,
|
|
limit=req_data.limit,
|
|
),
|
|
)
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
return dump_response(ConversationWithSummaryPaginationResponse, page)
|
|
|
|
|
|
@console_ns.route("/apps/<uuid:app_id>/chat-conversations/<uuid:conversation_id>")
|
|
class ChatConversationDetailApi(Resource):
|
|
@error_responses(AppNotFoundError, ConversationNotFoundHTTPError)
|
|
@console_ns.doc("get_chat_conversation")
|
|
@console_ns.doc(description="Get chat conversation details")
|
|
@console_ns.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
|
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[ConversationDetailResponse.__name__])
|
|
@console_ns.response(HTTPStatus.UNAUTHORIZED, "Authentication required")
|
|
@console_ns.response(HTTPStatus.FORBIDDEN, "Insufficient permissions")
|
|
@console_ns.response(HTTPStatus.NOT_FOUND, "App or conversation not found")
|
|
@console_account_admission(
|
|
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
|
|
rbac_checks=(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()),),
|
|
)
|
|
def get(self, request_context: RequestContext, app_id: UUID, conversation_id: UUID) -> dict[str, object]:
|
|
services = application_services()
|
|
try:
|
|
detail = services.conversation_queries.get_console_detail(
|
|
request_context, str(app_id), str(conversation_id), kind="chat"
|
|
)
|
|
response = dump_response(ConversationDetailResponse, detail)
|
|
# Preserve the Console read receipt only after rendering succeeds.
|
|
services.conversation_commands.mark_console_read(
|
|
request_context, str(app_id), str(conversation_id), kind="chat"
|
|
)
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
except ConversationNotExistsError as exc:
|
|
raise ConversationNotFoundHTTPError() from exc
|
|
return response
|
|
|
|
@error_responses(AppNotFoundError, ConversationNotFoundHTTPError)
|
|
@console_ns.doc("delete_chat_conversation")
|
|
@console_ns.doc(description="Delete a chat conversation")
|
|
@console_ns.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
|
@console_ns.response(HTTPStatus.NO_CONTENT, "Conversation deleted successfully")
|
|
@console_ns.response(HTTPStatus.UNAUTHORIZED, "Authentication required")
|
|
@console_ns.response(HTTPStatus.FORBIDDEN, "Insufficient permissions")
|
|
@console_ns.response(HTTPStatus.NOT_FOUND, "App or conversation not found")
|
|
@console_account_admission(
|
|
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
|
|
rbac_checks=(RBACCheck(RBACPermission.APP_EDIT, PlainApp()),),
|
|
)
|
|
def delete(self, request_context: RequestContext, app_id: UUID, conversation_id: UUID) -> tuple[str, HTTPStatus]:
|
|
try:
|
|
application_services().conversation_commands.delete_console(
|
|
request_context, str(app_id), str(conversation_id), kind="chat"
|
|
)
|
|
except ConsoleAppNotFoundError as exc:
|
|
raise AppNotFoundError(str(exc)) from exc
|
|
except ConversationNotExistsError as exc:
|
|
raise ConversationNotFoundHTTPError() from exc
|
|
return "", HTTPStatus.NO_CONTENT
|