Files
dify/api/controllers/web/message.py
T

331 lines
15 KiB
Python

import logging
from http import HTTPStatus
from typing import Literal
from uuid import UUID
from flask import Response
from pydantic import BaseModel, Field
from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery
from controllers.common.errors import InternalServerError, MessageFeedbackRatingRequiredError, NotFoundError
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.wraps import model_validate
from controllers.web import web_ns
from controllers.web.error import (
AppMoreLikeThisDisabledError,
AppSuggestedQuestionsAfterAnswerDisabledError,
AppUnavailableError,
CompletionRequestError,
NotChatAppError,
NotCompletionAppError,
ProviderModelCurrentlyNotSupportError,
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
from controllers.web.wraps import WebApiResource
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from extensions.ext_application_services import application_services
from fields.conversation_fields import ResultResponse
from fields.message_fields import SuggestedQuestionsResponse, WebMessageInfiniteScrollPagination
from graphon.model_runtime.errors.invoke import InvokeError
from libs import helper
from libs.exception import BaseHTTPException
from libs.external_api import error_responses
from models.enums import FeedbackRating
from models.model import App, AppMode, EndUser
from services.agent.errors import AgentVersionNotFoundError
from services.app_definition_query_service import AppDefinitionUnavailableError
from services.entities.message_entities import MessageEndUser
from services.errors.app import MoreLikeThisDisabledError
from services.errors.app_model_config import AppModelConfigBrokenError
from services.errors.conversation import ConversationNotExistsError
from services.errors.message import (
FeedbackRatingRequiredError,
FirstMessageNotExistsError,
MessageActorNotFoundError,
MessageNotExistsError,
SuggestedQuestionsAfterAnswerDisabledError,
)
from services.message_more_like_this_service import (
MoreLikeThisConfigNotFoundError,
MoreLikeThisNotCompletionError,
)
logger = logging.getLogger(__name__)
class MessageMoreLikeThisQuery(BaseModel):
response_mode: Literal["blocking", "streaming"] = Field(
description="Response mode",
)
register_schema_models(web_ns, MessageListQuery, MessageFeedbackPayload, MessageMoreLikeThisQuery)
register_response_schema_models(
web_ns,
GeneratedAppResponse,
ResultResponse,
SuggestedQuestionsResponse,
WebMessageInfiniteScrollPagination,
)
@web_ns.route("/messages")
class MessageListApi(WebApiResource):
@error_responses(AppUnavailableError, NotChatAppError)
@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=query_params_from_model(MessageListQuery))
@web_ns.doc(
responses={
HTTPStatus.OK: "Success",
HTTPStatus.BAD_REQUEST: "Bad Request - Not a chat app or app unavailable",
HTTPStatus.UNAUTHORIZED: "Unauthorized",
HTTPStatus.NOT_FOUND: "Conversation, First Message, or End User Not Found",
HTTPStatus.INTERNAL_SERVER_ERROR: "Internal Server Error",
}
)
@web_ns.response(HTTPStatus.OK, "Success", web_ns.models[WebMessageInfiniteScrollPagination.__name__])
@model_validate(MessageListQuery)
def get(self, query: MessageListQuery, app_model: App, end_user: EndUser) -> dict[str, object]:
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
raise NotChatAppError()
try:
pagination = application_services().message_queries.get_page(
app_id=app_model.id,
app_owner_tenant_id=app_model.tenant_id,
actor=MessageEndUser(end_user_id=end_user.id),
conversation_id=query.conversation_id,
first_id=query.first_id,
limit=query.limit,
)
return helper.dump_response(WebMessageInfiniteScrollPagination, pagination)
except AppDefinitionUnavailableError as error:
raise AppUnavailableError() from error
except MessageActorNotFoundError as error:
raise NotFoundError("End user not found") from error
except ConversationNotExistsError as error:
raise NotFoundError("Conversation Not Exists.") from error
except FirstMessageNotExistsError as error:
raise NotFoundError("First Message Not Exists.") from error
@web_ns.route("/messages/<uuid:message_id>/feedbacks")
class MessageFeedbackApi(WebApiResource):
@error_responses(AppUnavailableError, MessageFeedbackRatingRequiredError)
@web_ns.doc("Create Message Feedback")
@web_ns.doc(description="Submit feedback (like/dislike) for a specific message.")
@web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}})
@web_ns.doc(
params={
"rating": {
"description": "Feedback rating",
"type": "string",
"enum": ["like", "dislike"],
"required": False,
},
"content": {"description": "Feedback content", "type": "string", "required": False},
}
)
@web_ns.doc(
responses={
HTTPStatus.OK: "Feedback submitted successfully",
HTTPStatus.BAD_REQUEST: (
"`message_feedback_rating_required`: Cannot revoke feedback that does not exist. "
"`app_unavailable`: App is no longer available."
),
HTTPStatus.UNAUTHORIZED: "Unauthorized",
HTTPStatus.FORBIDDEN: "Forbidden",
HTTPStatus.NOT_FOUND: "Message Not Found",
HTTPStatus.INTERNAL_SERVER_ERROR: "Internal Server Error",
}
)
@web_ns.response(HTTPStatus.OK, "Feedback submitted successfully", web_ns.models[ResultResponse.__name__])
@web_ns.expect(web_ns.models[MessageFeedbackPayload.__name__])
@model_validate(MessageFeedbackPayload)
def post(
self, payload: MessageFeedbackPayload, app_model: App, end_user: EndUser, message_id: UUID
) -> dict[str, object]:
message_id_str = str(message_id)
try:
application_services().message_feedbacks.set_feedback(
app_id=app_model.id,
app_owner_tenant_id=app_model.tenant_id,
message_id=message_id_str,
actor=MessageEndUser(end_user_id=end_user.id),
rating=FeedbackRating(payload.rating) if payload.rating else None,
content=payload.content,
)
except AppDefinitionUnavailableError as error:
raise AppUnavailableError() from error
except MessageActorNotFoundError as error:
raise NotFoundError("End user not found") from error
except MessageNotExistsError as error:
raise NotFoundError("Message Not Exists.") from error
except FeedbackRatingRequiredError as error:
raise MessageFeedbackRatingRequiredError() from error
return helper.dump_response(ResultResponse, {"result": "success"})
@web_ns.route("/messages/<uuid:message_id>/more-like-this")
class MessageMoreLikeThisApi(WebApiResource):
@error_responses(
AppMoreLikeThisDisabledError,
AppUnavailableError,
CompletionRequestError,
NotCompletionAppError,
ProviderModelCurrentlyNotSupportError,
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
@web_ns.doc("Generate More Like This")
@web_ns.doc(description="Generate a new completion similar to an existing message (completion apps only).")
@web_ns.doc(params=query_params_from_model(MessageMoreLikeThisQuery))
@web_ns.doc(
responses={
HTTPStatus.OK: "Success",
HTTPStatus.BAD_REQUEST: "Bad Request - Not a completion app, app unavailable, or model invocation failed",
HTTPStatus.UNAUTHORIZED: "Unauthorized",
HTTPStatus.FORBIDDEN: "Forbidden - Access denied or more-like-this disabled",
HTTPStatus.NOT_FOUND: "Message Not Found",
HTTPStatus.INTERNAL_SERVER_ERROR: "Internal Server Error",
}
)
@web_ns.response(HTTPStatus.OK, "Success", web_ns.models[GeneratedAppResponse.__name__])
@model_validate(MessageMoreLikeThisQuery)
def get(
self,
query: MessageMoreLikeThisQuery,
app_model: App,
end_user: EndUser,
message_id: UUID,
) -> Response:
if app_model.mode != AppMode.COMPLETION:
raise NotCompletionAppError()
message_id_str = str(message_id)
streaming = query.response_mode == "streaming"
try:
response = application_services().message_more_like_this.generate(
app_id=app_model.id,
app_owner_tenant_id=app_model.tenant_id,
actor=MessageEndUser(end_user_id=end_user.id),
message_id=message_id_str,
streaming=streaming,
)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except (AppDefinitionUnavailableError, MoreLikeThisConfigNotFoundError, AppModelConfigBrokenError) as error:
raise AppUnavailableError() from error
except MessageActorNotFoundError as error:
raise NotFoundError("End user not found") from error
except MoreLikeThisNotCompletionError as error:
raise NotCompletionAppError() from error
except MessageNotExistsError as caught_error:
raise NotFoundError("Message Not Exists.") from caught_error
except MoreLikeThisDisabledError as caught_error:
raise AppMoreLikeThisDisabledError() from caught_error
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) from ex
except QuotaExceededError as caught_error:
raise ProviderQuotaExceededError() from caught_error
except ModelCurrentlyNotSupportError as caught_error:
raise ProviderModelCurrentlyNotSupportError() from caught_error
except InvokeError as e:
raise CompletionRequestError(e.description) from e
except (BaseHTTPException, ValueError):
raise
except Exception as caught_error:
logger.exception("internal server error.")
raise InternalServerError() from caught_error
@web_ns.route("/messages/<uuid:message_id>/suggested-questions")
class MessageSuggestedQuestionApi(WebApiResource):
@error_responses(
AgentVersionNotFoundError,
AppSuggestedQuestionsAfterAnswerDisabledError,
AppUnavailableError,
CompletionRequestError,
NotChatAppError,
ProviderModelCurrentlyNotSupportError,
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
@web_ns.response(HTTPStatus.OK, "Success", web_ns.models[SuggestedQuestionsResponse.__name__])
@web_ns.doc("Get Suggested Questions")
@web_ns.doc(
description=(
"Get suggested follow-up questions after a message (chat apps only). "
"If no usable model can be resolved or the model call to generate questions fails, "
"the response is HTTP 200 with an empty data list. "
"Model invocation failures during history token counting instead return "
"HTTP 400 with `completion_request_error`."
)
)
@web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}})
@web_ns.doc(
responses={
HTTPStatus.OK: "Success",
HTTPStatus.BAD_REQUEST: (
"Bad Request - Not a chat app or app unavailable; "
"`completion_request_error` when model invocation fails while counting history tokens."
),
HTTPStatus.UNAUTHORIZED: "Unauthorized",
HTTPStatus.FORBIDDEN: "Forbidden - Access denied or suggested questions disabled",
HTTPStatus.NOT_FOUND: "App, End User, Message, or Conversation Not Found",
HTTPStatus.INTERNAL_SERVER_ERROR: "Internal Server Error",
}
)
def get(self, app_model: App, end_user: EndUser, message_id: UUID) -> dict[str, object]:
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
raise NotChatAppError()
message_id_str = str(message_id)
try:
questions = application_services().message_suggested_questions.get_suggested_questions(
app_id=app_model.id,
app_owner_tenant_id=app_model.tenant_id,
expected_app_mode=app_model.mode,
actor=MessageEndUser(end_user_id=end_user.id),
invoke_from="web-app",
message_id=message_id_str,
)
except AppDefinitionUnavailableError:
raise AppUnavailableError() from None
except MessageActorNotFoundError:
raise NotFoundError("End user not found") from None
except MessageNotExistsError as caught_error:
raise NotFoundError("Message not found") from caught_error
except ConversationNotExistsError as caught_error:
raise NotFoundError("Conversation not found") from caught_error
except SuggestedQuestionsAfterAnswerDisabledError as caught_error:
raise AppSuggestedQuestionsAfterAnswerDisabledError() from caught_error
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) from ex
except QuotaExceededError as caught_error:
raise ProviderQuotaExceededError() from caught_error
except ModelCurrentlyNotSupportError as caught_error:
raise ProviderModelCurrentlyNotSupportError() from caught_error
except InvokeError as e:
raise CompletionRequestError(e.description) from e
except AgentVersionNotFoundError:
# The legacy Agent config reader still owns this HTTP error.
# Remove this compatibility case when it exposes a domain error.
raise
except Exception as caught_error:
logger.exception("internal server error.")
raise InternalServerError() from caught_error
return helper.dump_response(SuggestedQuestionsResponse, {"data": questions})