You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. from flask_restx import Resource, reqparse
  2. from flask_restx.inputs import int_range
  3. from sqlalchemy.orm import Session
  4. from werkzeug.exceptions import BadRequest, NotFound
  5. import services
  6. from controllers.service_api import service_api_ns
  7. from controllers.service_api.app.error import NotChatAppError
  8. from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
  9. from core.app.entities.app_invoke_entities import InvokeFrom
  10. from extensions.ext_database import db
  11. from fields.conversation_fields import (
  12. build_conversation_delete_model,
  13. build_conversation_infinite_scroll_pagination_model,
  14. build_simple_conversation_model,
  15. )
  16. from fields.conversation_variable_fields import (
  17. build_conversation_variable_infinite_scroll_pagination_model,
  18. build_conversation_variable_model,
  19. )
  20. from libs.helper import uuid_value
  21. from models.model import App, AppMode, EndUser
  22. from services.conversation_service import ConversationService
  23. # Define parsers for conversation APIs
  24. conversation_list_parser = reqparse.RequestParser()
  25. conversation_list_parser.add_argument(
  26. "last_id", type=uuid_value, location="args", help="Last conversation ID for pagination"
  27. )
  28. conversation_list_parser.add_argument(
  29. "limit",
  30. type=int_range(1, 100),
  31. required=False,
  32. default=20,
  33. location="args",
  34. help="Number of conversations to return",
  35. )
  36. conversation_list_parser.add_argument(
  37. "sort_by",
  38. type=str,
  39. choices=["created_at", "-created_at", "updated_at", "-updated_at"],
  40. required=False,
  41. default="-updated_at",
  42. location="args",
  43. help="Sort order for conversations",
  44. )
  45. conversation_rename_parser = reqparse.RequestParser()
  46. conversation_rename_parser.add_argument("name", type=str, required=False, location="json", help="New conversation name")
  47. conversation_rename_parser.add_argument(
  48. "auto_generate", type=bool, required=False, default=False, location="json", help="Auto-generate conversation name"
  49. )
  50. conversation_variables_parser = reqparse.RequestParser()
  51. conversation_variables_parser.add_argument(
  52. "last_id", type=uuid_value, location="args", help="Last variable ID for pagination"
  53. )
  54. conversation_variables_parser.add_argument(
  55. "limit", type=int_range(1, 100), required=False, default=20, location="args", help="Number of variables to return"
  56. )
  57. conversation_variable_update_parser = reqparse.RequestParser()
  58. # using lambda is for passing the already-typed value without modification
  59. # if no lambda, it will be converted to string
  60. # the string cannot be converted using json.loads
  61. conversation_variable_update_parser.add_argument(
  62. "value", required=True, location="json", type=lambda x: x, help="New value for the conversation variable"
  63. )
  64. @service_api_ns.route("/conversations")
  65. class ConversationApi(Resource):
  66. @service_api_ns.expect(conversation_list_parser)
  67. @service_api_ns.doc("list_conversations")
  68. @service_api_ns.doc(description="List all conversations for the current user")
  69. @service_api_ns.doc(
  70. responses={
  71. 200: "Conversations retrieved successfully",
  72. 401: "Unauthorized - invalid API token",
  73. 404: "Last conversation not found",
  74. }
  75. )
  76. @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
  77. @service_api_ns.marshal_with(build_conversation_infinite_scroll_pagination_model(service_api_ns))
  78. def get(self, app_model: App, end_user: EndUser):
  79. """List all conversations for the current user.
  80. Supports pagination using last_id and limit parameters.
  81. """
  82. app_mode = AppMode.value_of(app_model.mode)
  83. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  84. raise NotChatAppError()
  85. args = conversation_list_parser.parse_args()
  86. try:
  87. with Session(db.engine) as session:
  88. return ConversationService.pagination_by_last_id(
  89. session=session,
  90. app_model=app_model,
  91. user=end_user,
  92. last_id=args["last_id"],
  93. limit=args["limit"],
  94. invoke_from=InvokeFrom.SERVICE_API,
  95. sort_by=args["sort_by"],
  96. )
  97. except services.errors.conversation.LastConversationNotExistsError:
  98. raise NotFound("Last Conversation Not Exists.")
  99. @service_api_ns.route("/conversations/<uuid:c_id>")
  100. class ConversationDetailApi(Resource):
  101. @service_api_ns.doc("delete_conversation")
  102. @service_api_ns.doc(description="Delete a specific conversation")
  103. @service_api_ns.doc(params={"c_id": "Conversation ID"})
  104. @service_api_ns.doc(
  105. responses={
  106. 204: "Conversation deleted successfully",
  107. 401: "Unauthorized - invalid API token",
  108. 404: "Conversation not found",
  109. }
  110. )
  111. @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
  112. @service_api_ns.marshal_with(build_conversation_delete_model(service_api_ns), code=204)
  113. def delete(self, app_model: App, end_user: EndUser, c_id):
  114. """Delete a specific conversation."""
  115. app_mode = AppMode.value_of(app_model.mode)
  116. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  117. raise NotChatAppError()
  118. conversation_id = str(c_id)
  119. try:
  120. ConversationService.delete(app_model, conversation_id, end_user)
  121. except services.errors.conversation.ConversationNotExistsError:
  122. raise NotFound("Conversation Not Exists.")
  123. return {"result": "success"}, 204
  124. @service_api_ns.route("/conversations/<uuid:c_id>/name")
  125. class ConversationRenameApi(Resource):
  126. @service_api_ns.expect(conversation_rename_parser)
  127. @service_api_ns.doc("rename_conversation")
  128. @service_api_ns.doc(description="Rename a conversation or auto-generate a name")
  129. @service_api_ns.doc(params={"c_id": "Conversation ID"})
  130. @service_api_ns.doc(
  131. responses={
  132. 200: "Conversation renamed successfully",
  133. 401: "Unauthorized - invalid API token",
  134. 404: "Conversation not found",
  135. }
  136. )
  137. @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
  138. @service_api_ns.marshal_with(build_simple_conversation_model(service_api_ns))
  139. def post(self, app_model: App, end_user: EndUser, c_id):
  140. """Rename a conversation or auto-generate a name."""
  141. app_mode = AppMode.value_of(app_model.mode)
  142. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  143. raise NotChatAppError()
  144. conversation_id = str(c_id)
  145. args = conversation_rename_parser.parse_args()
  146. try:
  147. return ConversationService.rename(app_model, conversation_id, end_user, args["name"], args["auto_generate"])
  148. except services.errors.conversation.ConversationNotExistsError:
  149. raise NotFound("Conversation Not Exists.")
  150. @service_api_ns.route("/conversations/<uuid:c_id>/variables")
  151. class ConversationVariablesApi(Resource):
  152. @service_api_ns.expect(conversation_variables_parser)
  153. @service_api_ns.doc("list_conversation_variables")
  154. @service_api_ns.doc(description="List all variables for a conversation")
  155. @service_api_ns.doc(params={"c_id": "Conversation ID"})
  156. @service_api_ns.doc(
  157. responses={
  158. 200: "Variables retrieved successfully",
  159. 401: "Unauthorized - invalid API token",
  160. 404: "Conversation not found",
  161. }
  162. )
  163. @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
  164. @service_api_ns.marshal_with(build_conversation_variable_infinite_scroll_pagination_model(service_api_ns))
  165. def get(self, app_model: App, end_user: EndUser, c_id):
  166. """List all variables for a conversation.
  167. Conversational variables are only available for chat applications.
  168. """
  169. # conversational variable only for chat app
  170. app_mode = AppMode.value_of(app_model.mode)
  171. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  172. raise NotChatAppError()
  173. conversation_id = str(c_id)
  174. args = conversation_variables_parser.parse_args()
  175. try:
  176. return ConversationService.get_conversational_variable(
  177. app_model, conversation_id, end_user, args["limit"], args["last_id"]
  178. )
  179. except services.errors.conversation.ConversationNotExistsError:
  180. raise NotFound("Conversation Not Exists.")
  181. @service_api_ns.route("/conversations/<uuid:c_id>/variables/<uuid:variable_id>")
  182. class ConversationVariableDetailApi(Resource):
  183. @service_api_ns.expect(conversation_variable_update_parser)
  184. @service_api_ns.doc("update_conversation_variable")
  185. @service_api_ns.doc(description="Update a conversation variable's value")
  186. @service_api_ns.doc(params={"c_id": "Conversation ID", "variable_id": "Variable ID"})
  187. @service_api_ns.doc(
  188. responses={
  189. 200: "Variable updated successfully",
  190. 400: "Bad request - type mismatch",
  191. 401: "Unauthorized - invalid API token",
  192. 404: "Conversation or variable not found",
  193. }
  194. )
  195. @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
  196. @service_api_ns.marshal_with(build_conversation_variable_model(service_api_ns))
  197. def put(self, app_model: App, end_user: EndUser, c_id, variable_id):
  198. """Update a conversation variable's value.
  199. Allows updating the value of a specific conversation variable.
  200. The value must match the variable's expected type.
  201. """
  202. app_mode = AppMode.value_of(app_model.mode)
  203. if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
  204. raise NotChatAppError()
  205. conversation_id = str(c_id)
  206. variable_id = str(variable_id)
  207. args = conversation_variable_update_parser.parse_args()
  208. try:
  209. return ConversationService.update_conversation_variable(
  210. app_model, conversation_id, variable_id, end_user, args["value"]
  211. )
  212. except services.errors.conversation.ConversationNotExistsError:
  213. raise NotFound("Conversation Not Exists.")
  214. except services.errors.conversation.ConversationVariableNotExistsError:
  215. raise NotFound("Conversation Variable Not Exists.")
  216. except services.errors.conversation.ConversationVariableTypeMismatchError as e:
  217. raise BadRequest(str(e))