document_schedule.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. """
  2. Document Schedule API
  3. Manage scheduled document status changes
  4. """
  5. from datetime import datetime, timezone
  6. from flask import request
  7. from flask_login import current_user
  8. from flask_restx import Resource, fields, marshal_with, reqparse
  9. from werkzeug.exceptions import BadRequest, Forbidden, NotFound
  10. from controllers.console import api
  11. from controllers.console.datasets.error import InvalidActionError
  12. from controllers.console.wraps import (
  13. account_initialization_required,
  14. cloud_edition_billing_rate_limit_check,
  15. setup_required,
  16. )
  17. from libs.login import login_required
  18. from services.dataset_operation_log_service import DatasetOperationLogService, DatasetOperationType
  19. from services.dataset_service import DatasetService, DocumentService
  20. from services.document_schedule_service import DocumentScheduleService, ScheduleAction
  21. # Response fields for schedule
  22. schedule_fields = {
  23. "schedule_id": fields.String,
  24. "document_id": fields.String,
  25. "dataset_id": fields.String,
  26. "action": fields.String,
  27. "scheduled_at": fields.String,
  28. "created_by": fields.String,
  29. "created_at": fields.String,
  30. "status": fields.String,
  31. "metadata": fields.Raw,
  32. "cancelled_by": fields.String,
  33. "cancelled_at": fields.String,
  34. "completed_at": fields.String,
  35. "error_message": fields.String,
  36. }
  37. schedule_list_fields = {
  38. "schedules": fields.List(fields.Nested(schedule_fields)),
  39. "total": fields.Integer,
  40. }
  41. class DocumentScheduleResource(Resource):
  42. """Base resource for document schedule operations"""
  43. pass
  44. class DocumentScheduleListApi(DocumentScheduleResource):
  45. """API for listing and creating document schedules"""
  46. @setup_required
  47. @login_required
  48. @account_initialization_required
  49. @marshal_with(schedule_list_fields)
  50. def get(self, dataset_id: str, document_id: str):
  51. """
  52. Get all schedules for a document
  53. Args:
  54. dataset_id: Dataset ID
  55. document_id: Document ID
  56. Returns:
  57. dict: List of schedules
  58. """
  59. dataset_id_str = str(dataset_id)
  60. document_id_str = str(document_id)
  61. # Get dataset and check permissions
  62. dataset = DatasetService.get_dataset(dataset_id_str)
  63. if not dataset:
  64. raise NotFound("Dataset not found.")
  65. DatasetService.check_dataset_permission(dataset, current_user)
  66. # Get document
  67. document = DocumentService.get_document(dataset_id_str, document_id_str)
  68. if not document:
  69. raise NotFound("Document not found.")
  70. # Parse query parameters
  71. parser = reqparse.RequestParser()
  72. parser.add_argument("include_completed", type=bool, default=False, location="args")
  73. args = parser.parse_args()
  74. # Get schedules from Redis
  75. schedules = DocumentScheduleService.get_document_schedules(
  76. document_id_str, include_completed=args["include_completed"]
  77. )
  78. return {"schedules": schedules, "total": len(schedules)}
  79. @setup_required
  80. @login_required
  81. @account_initialization_required
  82. @cloud_edition_billing_rate_limit_check("knowledge")
  83. @marshal_with(schedule_fields)
  84. def post(self, dataset_id: str, document_id: str):
  85. """
  86. Create a scheduled document status change
  87. Args:
  88. dataset_id: Dataset ID
  89. document_id: Document ID
  90. Request Body:
  91. {
  92. "action": "enable|disable|archive",
  93. "scheduled_at": "2024-01-01T00:00:00Z",
  94. "metadata": {"reason": "...", "notes": "..."}
  95. }
  96. Returns:
  97. dict: Created schedule details
  98. """
  99. dataset_id_str = str(dataset_id)
  100. document_id_str = str(document_id)
  101. # Get dataset and check permissions
  102. dataset = DatasetService.get_dataset(dataset_id_str)
  103. if not dataset:
  104. raise NotFound("Dataset not found.")
  105. if not current_user.is_dataset_editor:
  106. raise Forbidden()
  107. DatasetService.check_dataset_permission(dataset, current_user)
  108. DatasetService.check_dataset_model_setting(dataset)
  109. # Get document
  110. document = DocumentService.get_document(dataset_id_str, document_id_str)
  111. if not document:
  112. raise NotFound("Document not found.")
  113. # Parse request body
  114. data = request.get_json()
  115. if not data:
  116. raise BadRequest("Request body is required")
  117. action_str = data.get("action")
  118. scheduled_at_str = data.get("scheduled_at")
  119. metadata = data.get("metadata", {})
  120. # Validate action
  121. try:
  122. action = ScheduleAction(action_str)
  123. except ValueError:
  124. raise BadRequest(f"Invalid action: {action_str}. Must be one of: enable, disable, archive")
  125. # Validate scheduled_at
  126. if not scheduled_at_str:
  127. raise BadRequest("scheduled_at is required")
  128. try:
  129. scheduled_at = datetime.fromisoformat(scheduled_at_str.replace("Z", "+00:00"))
  130. except (ValueError, AttributeError):
  131. raise BadRequest("Invalid scheduled_at format. Use ISO 8601 format (e.g., 2024-01-01T00:00:00Z)")
  132. # Check if scheduled time is in the future
  133. if scheduled_at <= datetime.now(timezone.utc):
  134. raise BadRequest("scheduled_at must be in the future")
  135. # Create schedule
  136. try:
  137. schedule_id = DocumentScheduleService.add_schedule(
  138. document_id=document_id_str,
  139. dataset_id=dataset_id_str,
  140. tenant_id=str(dataset.tenant_id),
  141. action=action,
  142. scheduled_at=scheduled_at,
  143. created_by=str(current_user.id),
  144. metadata=metadata,
  145. )
  146. # Log operation
  147. DatasetOperationLogService.log_operation(
  148. tenant_id=str(dataset.tenant_id),
  149. account_id=str(current_user.id),
  150. action=DatasetOperationType.DOCUMENT_SCHEDULE_CREATE,
  151. dataset_id=dataset_id_str,
  152. resource_type="document",
  153. resource_id=document_id_str,
  154. metadata={
  155. "schedule_id": schedule_id,
  156. "action": action.value,
  157. "scheduled_at": scheduled_at_str,
  158. },
  159. )
  160. # Get created schedule
  161. schedule = DocumentScheduleService.get_schedule(schedule_id)
  162. return schedule
  163. except Exception as e:
  164. raise InvalidActionError(str(e))
  165. class DocumentScheduleDetailApi(DocumentScheduleResource):
  166. """API for managing a specific schedule"""
  167. @setup_required
  168. @login_required
  169. @account_initialization_required
  170. @marshal_with(schedule_fields)
  171. def get(self, dataset_id: str, document_id: str, schedule_id: str):
  172. """
  173. Get schedule details
  174. Args:
  175. dataset_id: Dataset ID
  176. document_id: Document ID
  177. schedule_id: Schedule ID
  178. Returns:
  179. dict: Schedule details
  180. """
  181. dataset_id_str = str(dataset_id)
  182. # Get dataset and check permissions
  183. dataset = DatasetService.get_dataset(dataset_id_str)
  184. if not dataset:
  185. raise NotFound("Dataset not found.")
  186. DatasetService.check_dataset_permission(dataset, current_user)
  187. # Get schedule
  188. schedule = DocumentScheduleService.get_schedule(schedule_id)
  189. if not schedule:
  190. raise NotFound("Schedule not found.")
  191. # Verify the schedule belongs to this document
  192. if schedule.get("document_id") != str(document_id) or schedule.get("dataset_id") != dataset_id_str:
  193. raise NotFound("Schedule not found.")
  194. return schedule
  195. @setup_required
  196. @login_required
  197. @account_initialization_required
  198. @cloud_edition_billing_rate_limit_check("knowledge")
  199. def delete(self, dataset_id: str, document_id: str, schedule_id: str):
  200. """
  201. Cancel a scheduled task
  202. Args:
  203. dataset_id: Dataset ID
  204. document_id: Document ID
  205. schedule_id: Schedule ID
  206. Returns:
  207. dict: Success message
  208. """
  209. dataset_id_str = str(dataset_id)
  210. document_id_str = str(document_id)
  211. # Get dataset and check permissions
  212. dataset = DatasetService.get_dataset(dataset_id_str)
  213. if not dataset:
  214. raise NotFound("Dataset not found.")
  215. if not current_user.is_dataset_editor:
  216. raise Forbidden()
  217. DatasetService.check_dataset_permission(dataset, current_user)
  218. # Get schedule to verify it exists and belongs to this document
  219. schedule = DocumentScheduleService.get_schedule(schedule_id)
  220. if not schedule:
  221. raise NotFound("Schedule not found.")
  222. if schedule.get("document_id") != document_id_str or schedule.get("dataset_id") != dataset_id_str:
  223. raise NotFound("Schedule not found.")
  224. # Cancel schedule
  225. success = DocumentScheduleService.cancel_schedule(schedule_id, str(current_user.id))
  226. if not success:
  227. raise InvalidActionError("Failed to cancel schedule")
  228. # Log operation
  229. DatasetOperationLogService.log_operation(
  230. tenant_id=str(dataset.tenant_id),
  231. account_id=str(current_user.id),
  232. action=DatasetOperationType.DOCUMENT_SCHEDULE_CANCEL,
  233. dataset_id=dataset_id_str,
  234. resource_type="document",
  235. resource_id=document_id_str,
  236. metadata={
  237. "schedule_id": schedule_id,
  238. "action": schedule.get("action"),
  239. "scheduled_at": schedule.get("scheduled_at"),
  240. },
  241. )
  242. return {"result": "success", "message": "Schedule cancelled successfully"}, 200
  243. class DocumentScheduleStatisticsApi(DocumentScheduleResource):
  244. """API for getting schedule statistics"""
  245. @setup_required
  246. @login_required
  247. @account_initialization_required
  248. def get(self, dataset_id: str):
  249. """
  250. Get schedule statistics for a dataset
  251. Args:
  252. dataset_id: Dataset ID
  253. Returns:
  254. dict: Statistics
  255. """
  256. dataset_id_str = str(dataset_id)
  257. # Get dataset and check permissions
  258. dataset = DatasetService.get_dataset(dataset_id_str)
  259. if not dataset:
  260. raise NotFound("Dataset not found.")
  261. DatasetService.check_dataset_permission(dataset, current_user)
  262. # Get statistics
  263. stats = DocumentScheduleService.get_statistics(tenant_id=str(dataset.tenant_id))
  264. return stats, 200
  265. # Register API routes
  266. api.add_resource(
  267. DocumentScheduleListApi,
  268. "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/schedules",
  269. )
  270. api.add_resource(
  271. DocumentScheduleDetailApi,
  272. "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/schedules/<string:schedule_id>",
  273. )
  274. api.add_resource(
  275. DocumentScheduleStatisticsApi,
  276. "/datasets/<uuid:dataset_id>/schedules/statistics",
  277. )