1 package com.acumenvelocity.ath.controller;
2
3 import java.net.URI;
4 import java.util.ArrayList;
5 import java.util.Date;
6 import java.util.List;
7 import java.util.UUID;
8
9 import com.acumenvelocity.ath.common.AthUtil;
10 import com.acumenvelocity.ath.common.ControllerUtil;
11 import com.acumenvelocity.ath.common.Response;
12 import com.acumenvelocity.ath.model.ModelInfo;
13 import com.acumenvelocity.ath.model.ModelInfosWrapper;
14 import com.acumenvelocity.ath.model.PaginationInfo;
15 import com.acumenvelocity.ath.model.TrainModelRequest;
16 import com.acumenvelocity.ath.model.TrainingCompletedResponse;
17 import com.acumenvelocity.ath.model.TrainingFailedResponse;
18 import com.acumenvelocity.ath.model.TrainingJobResponse;
19 import com.acumenvelocity.ath.model.TrainingJobStatus;
20 import com.acumenvelocity.ath.model.TrainingStatusResponse;
21 import com.acumenvelocity.ath.service.AutoMlTrainingService;
22 import com.fasterxml.jackson.databind.JsonNode;
23
24 import io.swagger.oas.inflector.models.RequestContext;
25 import io.swagger.oas.inflector.models.ResponseContext;
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 public class AutoMlController {
54
55 private final AutoMlTrainingService trainingService;
56
57
58
59
60
61
62 public AutoMlController(AutoMlTrainingService trainingService) {
63 this.trainingService = trainingService;
64 }
65
66
67
68
69
70 public AutoMlController() {
71 this.trainingService = new AutoMlTrainingService();
72 }
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126 public ResponseContext trainModel(RequestContext request, JsonNode bodyNode) {
127 try {
128 if (!ControllerUtil.checkParam(bodyNode)) {
129 return Response.error(400, "Invalid request parameter, bodyNode is null");
130 }
131
132 TrainModelRequest body = AthUtil.safeFromJsonNode(bodyNode,
133 TrainModelRequest.class, null);
134
135 if (body == null) {
136 return Response.error(400, "Invalid request body");
137 }
138
139 UUID trainingJobId = body.getTrainingJobId();
140 String modelName = body.getModelName();
141 String srcLang = body.getSrcLang();
142 String trgLang = body.getTrgLang();
143 List<URI> gcsBuckets = body.getGcsBuckets();
144 String projectId = body.getProjectId();
145 String location = body.getLocation();
146 UUID userId = body.getUserId();
147
148 if (!ControllerUtil.checkParam(trainingJobId)) {
149 return Response.error(400, "training_job_id is required");
150 }
151
152 if (modelName == null || modelName.trim().isEmpty()) {
153 return Response.error(400, "model_name is required");
154 }
155
156 if (srcLang == null || srcLang.trim().isEmpty()) {
157 return Response.error(400, "src_lang is required");
158 }
159
160 if (trgLang == null || trgLang.trim().isEmpty()) {
161 return Response.error(400, "trg_lang is required");
162 }
163
164 if (gcsBuckets == null || gcsBuckets.isEmpty()) {
165 return Response.error(400, "gcs_buckets list is required and cannot be empty");
166 }
167
168 if (projectId == null || projectId.trim().isEmpty()) {
169 return Response.error(400, "project_id is required");
170 }
171
172 if (location == null || location.trim().isEmpty()) {
173 return Response.error(400, "location is required");
174 }
175
176 if (!ControllerUtil.checkParam(userId)) {
177 return Response.error(400, "user_id is required");
178 }
179
180
181 for (URI bucketUri : gcsBuckets) {
182 if (!bucketUri.toString().startsWith("gs://")) {
183 return Response.error(400, "Invalid GCS bucket URI: " + bucketUri +
184 ". Must start with gs://");
185 }
186 }
187
188
189 if (trainingService.trainingJobExists(trainingJobId)) {
190 return Response.error(409, "Training job with ID " + trainingJobId +
191 " already exists");
192 }
193
194
195 trainingService.initiateTraining(
196 trainingJobId,
197 modelName,
198 srcLang,
199 trgLang,
200 gcsBuckets,
201 projectId,
202 location,
203 userId);
204
205
206 TrainingJobResponse response = new TrainingJobResponse()
207 .status(TrainingJobResponse.StatusEnum.SCANNING_BUCKETS)
208 .trainingJobId(trainingJobId)
209 .statusUrl("/automl/model/train/" + trainingJobId + "/status")
210 .submittedAt(new Date());
211
212 ResponseContext responseContext = new ResponseContext()
213 .status(202)
214 .entity(response);
215
216 responseContext.getHeaders().put("Location",
217 List.of("/automl/model/train/" + trainingJobId + "/status"));
218
219 return responseContext;
220
221 } catch (IllegalArgumentException e) {
222 return Response.error(400, e.getMessage());
223
224 } catch (Exception e) {
225 return Response.error(500, e, "Error initiating model training");
226 }
227 }
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269 public ResponseContext getTrainingStatus(RequestContext request, UUID trainingJobId) {
270 try {
271 if (!ControllerUtil.checkParam(trainingJobId)) {
272 return Response.error(400, "Invalid training_job_id format");
273 }
274
275
276 if (!trainingService.trainingJobExists(trainingJobId)) {
277 return Response.error(404, "Training job not found: " + trainingJobId);
278 }
279
280
281 TrainingJobStatus status = trainingService.getTrainingStatus(trainingJobId);
282
283 if (status == null) {
284 return Response.error(404, "Training job not found: " + trainingJobId);
285 }
286
287 switch (status) {
288 case SCANNING_BUCKETS:
289 case PREPARING_DATASET:
290 case IMPORTING_DATA:
291 case TRAINING:
292
293 TrainingStatusResponse inProgressResponse = new TrainingStatusResponse()
294 .status(TrainingStatusResponse.StatusEnum.fromValue(status.name()))
295 .trainingJobId(trainingJobId)
296 .progress(trainingService.getProgress(trainingJobId))
297 .startedAt(trainingService.getStartedAt(trainingJobId))
298 .currentPhase(trainingService.getCurrentPhase(trainingJobId));
299
300 ResponseContext inProgressContext = new ResponseContext()
301 .status(202)
302 .entity(inProgressResponse);
303
304
305 int retryAfter = getRetryAfterSeconds(status);
306 inProgressContext.getHeaders().put("Retry-After", List.of(String.valueOf(retryAfter)));
307
308 return inProgressContext;
309
310 case COMPLETED:
311
312 TrainingCompletedResponse completedResponse = new TrainingCompletedResponse()
313 .status(TrainingCompletedResponse.StatusEnum.COMPLETED)
314 .trainingJobId(trainingJobId)
315 .modelId(trainingService.getModelId(trainingJobId))
316 .modelName(trainingService.getModelName(trainingJobId))
317 .datasetId(trainingService.getDatasetId(trainingJobId))
318 .completedAt(trainingService.getCompletedAt(trainingJobId))
319 .datasetStats(trainingService.getDatasetStats(trainingJobId));
320
321 return new ResponseContext()
322 .status(200)
323 .entity(completedResponse);
324
325 case FAILED:
326
327 TrainingFailedResponse failedResponse = new TrainingFailedResponse()
328 .status(TrainingFailedResponse.StatusEnum.FAILED)
329 .trainingJobId(trainingJobId)
330 .errorType(trainingService.getErrorType(trainingJobId))
331 .errorMessage(trainingService.getErrorMessage(trainingJobId))
332 .failedAt(trainingService.getFailedAt(trainingJobId));
333
334 return new ResponseContext()
335 .status(500)
336 .entity(failedResponse);
337
338 default:
339 return Response.error(500, "Unknown training status: " + status);
340 }
341
342 } catch (Exception e) {
343 return Response.error(500, e, "Error retrieving training status");
344 }
345 }
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376 public ResponseContext cancelTraining(RequestContext request, UUID trainingJobId) {
377 try {
378
379 if (!ControllerUtil.checkParam(trainingJobId)) {
380 return Response.error(400, "Invalid training_job_id format");
381 }
382
383
384 if (!trainingService.trainingJobExists(trainingJobId)) {
385 return Response.error(404, "Training job not found: " + trainingJobId);
386 }
387
388
389 TrainingJobStatus status = trainingService.getTrainingStatus(trainingJobId);
390
391
392 if (TrainingJobStatus.COMPLETED.equals(status)) {
393 return Response.error(409,
394 "Cannot cancel training - training has already completed successfully");
395 }
396
397 if (TrainingJobStatus.FAILED.equals(status)) {
398 return Response.error(409,
399 "Cannot cancel training - training has already failed");
400 }
401
402 if (TrainingJobStatus.CANCELLED.equals(status)) {
403 return Response.error(409,
404 "Cannot cancel training - training has already been cancelled");
405 }
406
407
408 trainingService.cancelTraining(trainingJobId);
409
410
411 return new ResponseContext().status(204);
412
413 } catch (Exception e) {
414 return Response.error(500, e, "Error cancelling training job");
415 }
416 }
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443 public ResponseContext getModels(RequestContext request, Integer page, Integer pageSize) {
444 try {
445 List<ModelInfo> allModels = trainingService.getAllModels();
446 ModelInfosWrapper wrapper = new ModelInfosWrapper();
447
448
449 if (allModels.isEmpty()) {
450 wrapper.models(new ArrayList<>())
451 .pagination(new PaginationInfo()
452 .page(1)
453 .pageSize(0)
454 .totalItems(0L)
455 .totalPages(0)
456 .hasNext(false)
457 .hasPrevious(false));
458
459 return Response.success(200, wrapper);
460 }
461
462
463 long totalItems = allModels.size();
464 int size = (pageSize != null) ? Math.max(1, Math.min(100, pageSize)) : (int) totalItems;
465 int totalPages = (int) Math.ceil((double) totalItems / size);
466
467 int pageNum;
468 List<ModelInfo> resultModels;
469
470 if (page == null && pageSize == null) {
471
472 pageNum = 1;
473 resultModels = allModels;
474 totalPages = 1;
475
476 } else {
477
478 pageNum = (page != null) ? Math.max(1, Math.min(page, Math.max(1, totalPages))) : 1;
479 int start = (pageNum - 1) * size;
480 int end = Math.min(start + size, allModels.size());
481 resultModels = allModels.subList(start, end);
482 }
483
484
485 PaginationInfo pagination = new PaginationInfo()
486 .page(pageNum)
487 .pageSize(size)
488 .totalItems(totalItems)
489 .totalPages(totalPages)
490 .hasNext(pageNum < totalPages)
491 .hasPrevious(pageNum > 1);
492
493 wrapper.models(resultModels)
494 .pagination(pagination);
495
496 return Response.success(200, wrapper);
497
498 } catch (Exception e) {
499 return Response.error(500, e, "Error fetching trained models");
500 }
501 }
502
503
504
505
506
507
508
509 private int getRetryAfterSeconds(TrainingJobStatus status) {
510 switch (status) {
511 case SCANNING_BUCKETS:
512 case PREPARING_DATASET:
513 return 10;
514
515 case IMPORTING_DATA:
516 return 60;
517
518 case TRAINING:
519 return 300;
520
521 default:
522 return 30;
523 }
524 }
525 }