Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import logging
  2. from flask_restx import fields, marshal_with, reqparse
  3. from flask_restx.inputs import int_range
  4. from werkzeug.exceptions import InternalServerError, NotFound
  5. from controllers.web import web_ns
  6. from controllers.web.error import (
  7. AppMoreLikeThisDisabledError,
  8. AppSuggestedQuestionsAfterAnswerDisabledError,
  9. CompletionRequestError,
  10. NotChatAppError,
  11. NotCompletionAppError,
  12. ProviderModelCurrentlyNotSupportError,
  13. ProviderNotInitializeError,
  14. ProviderQuotaExceededError,
  15. )
  16. from controllers.web.wraps import WebApiResource
  17. from core.app.entities.app_invoke_entities import InvokeFrom
  18. from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
  19. from core.model_runtime.errors.invoke import InvokeError
  20. from fields.conversation_fields import message_file_fields
  21. from fields.message_fields import agent_thought_fields, feedback_fields, retriever_resource_fields
  22. from fields.raws import FilesContainedField
  23. from libs import helper
  24. from libs.helper import TimestampField, uuid_value
  25. from models.model import AppMode
  26. from services.app_generate_service import AppGenerateService
  27. from services.errors.app import MoreLikeThisDisabledError
  28. from services.errors.conversation import ConversationNotExistsError
  29. from services.errors.message import (
  30. FirstMessageNotExistsError,
  31. MessageNotExistsError,
  32. SuggestedQuestionsAfterAnswerDisabledError,
  33. )
  34. from services.message_service import MessageService
  35. logger = logging.getLogger(__name__)
  36. @web_ns.route("/messages")
  37. class MessageListApi(WebApiResource):
  38. message_fields = {
  39. "id": fields.String,
  40. "conversation_id": fields.String,
  41. "parent_message_id": fields.String,
  42. "inputs": FilesContainedField,
  43. "query": fields.String,
  44. "answer": fields.String(attribute="re_sign_file_url_answer"),
  45. "message_files": fields.List(fields.Nested(message_file_fields)),
  46. "feedback": fields.Nested(feedback_fields, attribute="user_feedback", allow_null=True),
  47. "retriever_resources": fields.List(fields.Nested(retriever_resource_fields)),
  48. "created_at": TimestampField,
  49. "agent_thoughts": fields.List(fields.Nested(agent_thought_fields)),
  50. "metadata": fields.Raw(attribute="message_metadata_dict"),
  51. "status": fields.String,
  52. "error": fields.String,
  53. }
  54. message_infinite_scroll_pagination_fields = {
  55. "limit": fields.Integer,
  56. "has_more": fields.Boolean,
  57. "data": fields.List(fields.Nested(message_fields)),
  58. }
  59. @web_ns.doc("Get Message List")
  60. @web_ns.doc(description="Retrieve paginated list of messages from a conversation in a chat application.")
  61. @web_ns.doc(
  62. params={
  63. "conversation_id": {"description": "Conversation UUID", "type": "string", "required": True},
  64. "first_id": {"description": "First message ID for pagination", "type": "string", "required": False},
  65. "limit": {
  66. "description": "Number of messages to return (1-100)",
  67. "type": "integer",
  68. "required": False,
  69. "default": 20,
  70. },
  71. }
  72. )
  73. @web_ns.doc(
  74. responses={
  75. 200: "Success",
  76. 400: "Bad Request",
  77. 401: "Unauthorized",
  78. 403: "Forbidden",
  79. 404: "Conversation Not Found or Not a Chat App",
  80. 500: "Internal Server Error",
  81. }
  82. )
  83. @marshal_with(message_infinite_scroll_pagination_fields)
  84. def get(self, app_model, end_user):
  85. app_mode = AppMode.value_of(app_model.mode)
  86. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  87. raise NotChatAppError()
  88. parser = reqparse.RequestParser()
  89. parser.add_argument("conversation_id", required=True, type=uuid_value, location="args")
  90. parser.add_argument("first_id", type=uuid_value, location="args")
  91. parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
  92. args = parser.parse_args()
  93. try:
  94. return MessageService.pagination_by_first_id(
  95. app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
  96. )
  97. except ConversationNotExistsError:
  98. raise NotFound("Conversation Not Exists.")
  99. except FirstMessageNotExistsError:
  100. raise NotFound("First Message Not Exists.")
  101. @web_ns.route("/messages/<uuid:message_id>/feedbacks")
  102. class MessageFeedbackApi(WebApiResource):
  103. feedback_response_fields = {
  104. "result": fields.String,
  105. }
  106. @web_ns.doc("Create Message Feedback")
  107. @web_ns.doc(description="Submit feedback (like/dislike) for a specific message.")
  108. @web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}})
  109. @web_ns.doc(
  110. params={
  111. "rating": {
  112. "description": "Feedback rating",
  113. "type": "string",
  114. "enum": ["like", "dislike"],
  115. "required": False,
  116. },
  117. "content": {"description": "Feedback content/comment", "type": "string", "required": False},
  118. }
  119. )
  120. @web_ns.doc(
  121. responses={
  122. 200: "Feedback submitted successfully",
  123. 400: "Bad Request",
  124. 401: "Unauthorized",
  125. 403: "Forbidden",
  126. 404: "Message Not Found",
  127. 500: "Internal Server Error",
  128. }
  129. )
  130. @marshal_with(feedback_response_fields)
  131. def post(self, app_model, end_user, message_id):
  132. message_id = str(message_id)
  133. parser = reqparse.RequestParser()
  134. parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
  135. parser.add_argument("content", type=str, location="json", default=None)
  136. args = parser.parse_args()
  137. try:
  138. MessageService.create_feedback(
  139. app_model=app_model,
  140. message_id=message_id,
  141. user=end_user,
  142. rating=args.get("rating"),
  143. content=args.get("content"),
  144. )
  145. except MessageNotExistsError:
  146. raise NotFound("Message Not Exists.")
  147. return {"result": "success"}
  148. @web_ns.route("/messages/<uuid:message_id>/more-like-this")
  149. class MessageMoreLikeThisApi(WebApiResource):
  150. @web_ns.doc("Generate More Like This")
  151. @web_ns.doc(description="Generate a new completion similar to an existing message (completion apps only).")
  152. @web_ns.doc(
  153. params={
  154. "message_id": {"description": "Message UUID", "type": "string", "required": True},
  155. "response_mode": {
  156. "description": "Response mode",
  157. "type": "string",
  158. "enum": ["blocking", "streaming"],
  159. "required": True,
  160. },
  161. }
  162. )
  163. @web_ns.doc(
  164. responses={
  165. 200: "Success",
  166. 400: "Bad Request - Not a completion app or feature disabled",
  167. 401: "Unauthorized",
  168. 403: "Forbidden",
  169. 404: "Message Not Found",
  170. 500: "Internal Server Error",
  171. }
  172. )
  173. def get(self, app_model, end_user, message_id):
  174. if app_model.mode != "completion":
  175. raise NotCompletionAppError()
  176. message_id = str(message_id)
  177. parser = reqparse.RequestParser()
  178. parser.add_argument(
  179. "response_mode", type=str, required=True, choices=["blocking", "streaming"], location="args"
  180. )
  181. args = parser.parse_args()
  182. streaming = args["response_mode"] == "streaming"
  183. try:
  184. response = AppGenerateService.generate_more_like_this(
  185. app_model=app_model,
  186. user=end_user,
  187. message_id=message_id,
  188. invoke_from=InvokeFrom.WEB_APP,
  189. streaming=streaming,
  190. )
  191. return helper.compact_generate_response(response)
  192. except MessageNotExistsError:
  193. raise NotFound("Message Not Exists.")
  194. except MoreLikeThisDisabledError:
  195. raise AppMoreLikeThisDisabledError()
  196. except ProviderTokenNotInitError as ex:
  197. raise ProviderNotInitializeError(ex.description)
  198. except QuotaExceededError:
  199. raise ProviderQuotaExceededError()
  200. except ModelCurrentlyNotSupportError:
  201. raise ProviderModelCurrentlyNotSupportError()
  202. except InvokeError as e:
  203. raise CompletionRequestError(e.description)
  204. except ValueError as e:
  205. raise e
  206. except Exception:
  207. logger.exception("internal server error.")
  208. raise InternalServerError()
  209. @web_ns.route("/messages/<uuid:message_id>/suggested-questions")
  210. class MessageSuggestedQuestionApi(WebApiResource):
  211. suggested_questions_response_fields = {
  212. "data": fields.List(fields.String),
  213. }
  214. @web_ns.doc("Get Suggested Questions")
  215. @web_ns.doc(description="Get suggested follow-up questions after a message (chat apps only).")
  216. @web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}})
  217. @web_ns.doc(
  218. responses={
  219. 200: "Success",
  220. 400: "Bad Request - Not a chat app or feature disabled",
  221. 401: "Unauthorized",
  222. 403: "Forbidden",
  223. 404: "Message Not Found or Conversation Not Found",
  224. 500: "Internal Server Error",
  225. }
  226. )
  227. @marshal_with(suggested_questions_response_fields)
  228. def get(self, app_model, end_user, message_id):
  229. app_mode = AppMode.value_of(app_model.mode)
  230. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  231. raise NotCompletionAppError()
  232. message_id = str(message_id)
  233. try:
  234. questions = MessageService.get_suggested_questions_after_answer(
  235. app_model=app_model, user=end_user, message_id=message_id, invoke_from=InvokeFrom.WEB_APP
  236. )
  237. # questions is a list of strings, not a list of Message objects
  238. # so we can directly return it
  239. except MessageNotExistsError:
  240. raise NotFound("Message not found")
  241. except ConversationNotExistsError:
  242. raise NotFound("Conversation not found")
  243. except SuggestedQuestionsAfterAnswerDisabledError:
  244. raise AppSuggestedQuestionsAfterAnswerDisabledError()
  245. except ProviderTokenNotInitError as ex:
  246. raise ProviderNotInitializeError(ex.description)
  247. except QuotaExceededError:
  248. raise ProviderQuotaExceededError()
  249. except ModelCurrentlyNotSupportError:
  250. raise ProviderModelCurrentlyNotSupportError()
  251. except InvokeError as e:
  252. raise CompletionRequestError(e.description)
  253. except Exception:
  254. logger.exception("internal server error.")
  255. raise InternalServerError()
  256. return {"data": questions}