-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEmailMappingResource.java
More file actions
571 lines (501 loc) · 25.6 KB
/
EmailMappingResource.java
File metadata and controls
571 lines (501 loc) · 25.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
package de.tum.cit.aet.analysis.web;
import de.tum.cit.aet.analysis.domain.AnalyzedChunk;
import de.tum.cit.aet.analysis.domain.ExerciseEmailMapping;
import de.tum.cit.aet.analysis.domain.ExerciseTemplateAuthor;
import de.tum.cit.aet.analysis.dto.CreateEmailMappingRequestDTO;
import de.tum.cit.aet.analysis.dto.DismissEmailRequestDTO;
import de.tum.cit.aet.analysis.dto.EmailMappingDTO;
import de.tum.cit.aet.analysis.repository.AnalyzedChunkRepository;
import de.tum.cit.aet.analysis.repository.ExerciseEmailMappingRepository;
import de.tum.cit.aet.analysis.repository.ExerciseTemplateAuthorRepository;
import de.tum.cit.aet.analysis.service.cqi.CqiRecalculationService;
import de.tum.cit.aet.ai.dto.*;
import de.tum.cit.aet.repositoryProcessing.domain.Student;
import de.tum.cit.aet.repositoryProcessing.domain.TeamParticipation;
import de.tum.cit.aet.repositoryProcessing.dto.ClientResponseDTO;
import de.tum.cit.aet.repositoryProcessing.dto.StudentAnalysisDTO;
import de.tum.cit.aet.repositoryProcessing.repository.StudentRepository;
import de.tum.cit.aet.repositoryProcessing.repository.TeamParticipationRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* REST controller for managing manual email-to-student mappings.
* When a mapping is created or removed the CQI is recalculated
* from the already-persisted LLM scores (no new LLM call).
*/
@RestController
@RequestMapping("/api/exercises/{exerciseId}/email-mappings")
@Slf4j
@RequiredArgsConstructor
public class EmailMappingResource {
private final ExerciseEmailMappingRepository emailMappingRepository;
private final ExerciseTemplateAuthorRepository templateAuthorRepository;
private final AnalyzedChunkRepository analyzedChunkRepository;
private final TeamParticipationRepository teamParticipationRepository;
private final StudentRepository studentRepository;
private final CqiRecalculationService cqiRecalculationService;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Returns all email mappings for the given exercise.
*
* @param exerciseId the exercise ID
* @return list of email mapping DTOs
*/
@GetMapping
public ResponseEntity<List<EmailMappingDTO>> getAllMappings(@PathVariable Long exerciseId) {
List<EmailMappingDTO> dtos = emailMappingRepository.findAllByExerciseId(exerciseId)
.stream()
.map(m -> new EmailMappingDTO(m.getId(), m.getExerciseId(),
m.getGitEmail(), m.getStudentId(), m.getStudentName(),
m.getIsDismissed()))
.toList();
return ResponseEntity.ok(dtos);
}
/**
* Creates a new email mapping and recalculates CQI for the affected team.
*
* @param exerciseId the exercise ID
* @param request the mapping request with git email, student info and participation ID
* @return updated client response DTO with recalculated CQI
*/
@PostMapping
@Transactional
public ResponseEntity<ClientResponseDTO> createMapping(
@PathVariable Long exerciseId,
@RequestBody CreateEmailMappingRequestDTO request) {
log.info("POST createMapping for exerciseId={}, gitEmail={}, studentId={}",
exerciseId, request.gitEmail(), request.studentId());
// 1. Find the team participation (must exist before we resolve the student ID)
TeamParticipation participation = teamParticipationRepository
.findByExerciseIdAndTeam(exerciseId, request.teamParticipationId())
.orElseThrow(() -> new IllegalArgumentException(
"TeamParticipation not found for exercise " + exerciseId
+ " and team " + request.teamParticipationId()));
// 2. Resolve real Artemis student ID by name (client may send 0 as placeholder)
Long resolvedStudentId = request.studentId();
if (request.studentName() != null) {
List<Student> students = studentRepository.findAllByTeam(participation);
resolvedStudentId = students.stream()
.filter(s -> request.studentName().equals(s.getName()))
.map(Student::getId)
.findFirst()
.orElse(request.studentId());
}
if (resolvedStudentId == null || resolvedStudentId <= 0) {
return ResponseEntity.badRequest().build();
}
// 3. Check for existing mapping with the same email
String normalizedEmail = request.gitEmail().toLowerCase(Locale.ROOT);
if (emailMappingRepository.existsByExerciseIdAndGitEmail(exerciseId, normalizedEmail)) {
return ResponseEntity.status(409).build();
}
// 4. Save mapping with resolved ID
ExerciseEmailMapping mapping = new ExerciseEmailMapping(
exerciseId, normalizedEmail,
resolvedStudentId, request.studentName());
emailMappingRepository.save(mapping);
// 5. Update chunks: mark matching external chunks as non-external
List<AnalyzedChunk> chunks = analyzedChunkRepository.findByParticipation(participation);
List<AnalyzedChunk> remappedChunks = new ArrayList<>();
for (AnalyzedChunk chunk : chunks) {
if (isExternalChunkForEmail(chunk, normalizedEmail)) {
chunk.setIsExternalContributor(false);
chunk.setAuthorName(request.studentName());
remappedChunks.add(chunk);
}
}
analyzedChunkRepository.saveAll(remappedChunks);
// 6. Update target student's commit/line stats with the remapped chunks
if (!remappedChunks.isEmpty()) {
addChunkStatsToStudent(participation, request.studentName(), remappedChunks);
}
// 7. Recalculate CQI from persisted chunks
cqiRecalculationService.recalculateFromChunks(participation, chunks);
// 8. Return updated response
return ResponseEntity.ok(buildResponse(participation));
}
/**
* Dismisses an orphan email without assigning it to a student.
* Chunks are NOT mutated — the client uses the dismissed mapping
* to display them in a separate "Dismissed" section.
*
* @param exerciseId the exercise ID
* @param request the dismiss request with git email and participation ID
* @return updated client response DTO
*/
@PostMapping("/dismiss")
@Transactional
public ResponseEntity<ClientResponseDTO> dismissEmail(
@PathVariable Long exerciseId,
@RequestBody DismissEmailRequestDTO request) {
log.info("POST dismissEmail for exerciseId={}, gitEmail={}", exerciseId, request.gitEmail());
// 1. Normalize and check for duplicates
String normalizedEmail = request.gitEmail().toLowerCase(Locale.ROOT);
if (emailMappingRepository.existsByExerciseIdAndGitEmail(exerciseId, normalizedEmail)) {
return ResponseEntity.status(409).build();
}
// 2. Save dismissed mapping (no student)
ExerciseEmailMapping mapping = new ExerciseEmailMapping(exerciseId, normalizedEmail, true);
emailMappingRepository.save(mapping);
// 3. Recalculate so orphanCommitCount excludes dismissed emails
TeamParticipation participation = teamParticipationRepository
.findByExerciseIdAndTeam(exerciseId, request.teamParticipationId())
.orElse(null);
if (participation != null) {
List<AnalyzedChunk> chunks = analyzedChunkRepository.findByParticipation(participation);
cqiRecalculationService.recalculateFromChunks(participation, chunks);
return ResponseEntity.ok(buildResponse(participation));
}
return ResponseEntity.noContent().build();
}
/**
* Deletes an email mapping and recalculates CQI for affected teams.
*
* @param exerciseId the exercise ID
* @param mappingId the mapping ID to delete
* @return updated client response DTO, or 204 if no team was affected
*/
@DeleteMapping("/{mappingId}")
@Transactional
public ResponseEntity<ClientResponseDTO> deleteMapping(
@PathVariable Long exerciseId,
@PathVariable UUID mappingId) {
ExerciseEmailMapping mapping = emailMappingRepository.findById(mappingId)
.orElseThrow(() -> new IllegalArgumentException("Mapping not found: " + mappingId));
if (!mapping.getExerciseId().equals(exerciseId)) {
return ResponseEntity.notFound().build();
}
log.info("DELETE deleteMapping for exerciseId={}, gitEmail={}, studentId={}",
exerciseId, mapping.getGitEmail(), mapping.getStudentId());
emailMappingRepository.delete(mapping);
// Find all participations for this exercise and update chunks
List<TeamParticipation> participations = teamParticipationRepository
.findAllByExerciseId(exerciseId);
String emailLower = mapping.getGitEmail().toLowerCase(Locale.ROOT);
ClientResponseDTO lastResponse = null;
for (TeamParticipation participation : participations) {
List<AnalyzedChunk> chunks = analyzedChunkRepository.findByParticipation(participation);
List<AnalyzedChunk> orphanedChunks = new ArrayList<>();
// Build set of known student emails (from students + remaining mappings)
Set<String> knownEmails = buildKnownEmails(participation, exerciseId);
for (AnalyzedChunk chunk : chunks) {
String chunkEmail = chunk.getAuthorEmail() != null
? chunk.getAuthorEmail().toLowerCase(Locale.ROOT) : null;
if (emailLower.equals(chunkEmail)
&& !knownEmails.contains(chunkEmail)) {
chunk.setIsExternalContributor(true);
chunk.setAuthorName(chunk.getAuthorEmail());
orphanedChunks.add(chunk);
}
}
if (!orphanedChunks.isEmpty()) {
analyzedChunkRepository.saveAll(orphanedChunks);
if (!Boolean.TRUE.equals(mapping.getIsDismissed())) {
subtractChunkStatsFromStudent(participation, mapping.getStudentName(), orphanedChunks);
}
cqiRecalculationService.recalculateFromChunks(participation, chunks);
lastResponse = buildResponse(participation);
}
}
if (lastResponse != null) {
return ResponseEntity.ok(lastResponse);
}
return ResponseEntity.noContent().build();
}
// ================================================================
// Template Author endpoints
// ================================================================
/**
* DTO for the template author response / request.
*/
public record TemplateAuthorDTO(
String templateEmail,
Boolean autoDetected) {
}
/**
* Returns the configured template author for the given exercise.
*
* @param exerciseId the exercise ID
* @return template author DTO, or 200 with null body if not configured
*/
@GetMapping("/template-author")
public ResponseEntity<TemplateAuthorDTO> getTemplateAuthor(@PathVariable Long exerciseId) {
return templateAuthorRepository.findByExerciseId(exerciseId)
.map(ta -> ResponseEntity.ok(
new TemplateAuthorDTO(ta.getTemplateEmail(), ta.getAutoDetected())))
.orElse(ResponseEntity.ok(null));
}
/**
* Sets or updates the template author for an exercise.
* All affected teams' CQI is recalculated from persisted chunks.
*
* @param exerciseId the exercise ID
* @param request the template author request with email
* @return list of updated client response DTOs
*/
@PutMapping("/template-author")
@Transactional
public ResponseEntity<List<ClientResponseDTO>> setTemplateAuthor(
@PathVariable Long exerciseId,
@RequestBody TemplateAuthorDTO request) {
String newEmail = request.templateEmail().toLowerCase(Locale.ROOT);
log.info("PUT setTemplateAuthor for exerciseId={}, email={}", exerciseId, newEmail);
// Load or create template author entity
ExerciseTemplateAuthor ta = templateAuthorRepository.findByExerciseId(exerciseId)
.orElse(new ExerciseTemplateAuthor(exerciseId, newEmail, false));
String oldEmail = ta.getTemplateEmail();
ta.setTemplateEmail(newEmail);
ta.setAutoDetected(false); // Manual override
templateAuthorRepository.save(ta);
// Recalculate CQI for all teams of this exercise
List<TeamParticipation> participations = teamParticipationRepository
.findAllByExerciseId(exerciseId);
List<ClientResponseDTO> responses = new ArrayList<>();
for (TeamParticipation participation : participations) {
List<AnalyzedChunk> chunks = analyzedChunkRepository.findByParticipation(participation);
boolean changed = false;
// Build known emails so we don't accidentally orphan a known student
Set<String> knownEmails = buildKnownEmails(participation, exerciseId);
for (AnalyzedChunk chunk : chunks) {
String chunkEmail = chunk.getAuthorEmail() != null
? chunk.getAuthorEmail().toLowerCase(Locale.ROOT) : null;
if (oldEmail != null && oldEmail.equalsIgnoreCase(chunkEmail)
&& !newEmail.equalsIgnoreCase(chunkEmail)) {
// Old template email: only mark external if NOT a known student/mapping
boolean shouldBeExternal = !knownEmails.contains(
chunkEmail != null ? chunkEmail.toLowerCase(Locale.ROOT) : null);
chunk.setIsExternalContributor(shouldBeExternal);
changed = true;
}
if (newEmail.equalsIgnoreCase(chunkEmail)) {
// New template email → mark as external (template)
chunk.setIsExternalContributor(true);
changed = true;
}
}
if (changed) {
analyzedChunkRepository.saveAll(chunks);
}
cqiRecalculationService.recalculateFromChunks(participation, chunks);
responses.add(buildResponse(participation));
}
return ResponseEntity.ok(responses);
}
/**
* Removes the template author configuration for an exercise.
* Chunks from the old template author become regular orphans again.
*
* @param exerciseId the exercise ID
* @return list of updated client response DTOs
*/
@DeleteMapping("/template-author")
@Transactional
public ResponseEntity<List<ClientResponseDTO>> deleteTemplateAuthor(
@PathVariable Long exerciseId) {
ExerciseTemplateAuthor ta = templateAuthorRepository.findByExerciseId(exerciseId)
.orElse(null);
if (ta == null) {
return ResponseEntity.noContent().build();
}
String oldEmail = ta.getTemplateEmail().toLowerCase(Locale.ROOT);
log.info("DELETE deleteTemplateAuthor for exerciseId={}, email={}", exerciseId, oldEmail);
templateAuthorRepository.delete(ta);
// Recalculate CQI for all teams — old template chunks stay as external/orphan
// unless they match a student or email mapping
List<TeamParticipation> participations = teamParticipationRepository
.findAllByExerciseId(exerciseId);
List<ClientResponseDTO> responses = new ArrayList<>();
for (TeamParticipation participation : participations) {
List<AnalyzedChunk> chunks = analyzedChunkRepository.findByParticipation(participation);
// Unmark chunks whose old template email is actually a known student/mapping
Set<String> knownEmails = buildKnownEmails(participation, exerciseId);
boolean changed = false;
for (AnalyzedChunk chunk : chunks) {
String chunkEmail = chunk.getAuthorEmail() != null
? chunk.getAuthorEmail().toLowerCase(Locale.ROOT) : null;
if (oldEmail.equals(chunkEmail)
&& Boolean.TRUE.equals(chunk.getIsExternalContributor())
&& knownEmails.contains(chunkEmail)) {
chunk.setIsExternalContributor(false);
changed = true;
}
}
if (changed) {
analyzedChunkRepository.saveAll(chunks);
}
cqiRecalculationService.recalculateFromChunks(participation, chunks);
responses.add(buildResponse(participation));
}
return ResponseEntity.ok(responses);
}
/**
* Adds the commit/line stats from the given chunks to the named student.
* The linesAdded/linesDeleted split is distributed proportionally based on
* the student's existing ratio, since chunks only store total linesChanged.
*/
private void addChunkStatsToStudent(TeamParticipation participation, String studentName,
List<AnalyzedChunk> addedChunks) {
ChunkStatsDelta delta = computeChunkStats(addedChunks);
if (delta.commits == 0 && delta.linesChanged == 0) {
return;
}
studentRepository.findAllByTeam(participation).stream()
.filter(s -> studentName.equals(s.getName()))
.findFirst()
.ifPresent(student -> {
student.setCommitCount(safe(student.getCommitCount()) + delta.commits);
student.setLinesChanged(safe(student.getLinesChanged()) + delta.linesChanged);
applyLinesSplit(student, delta.linesChanged, true);
studentRepository.save(student);
});
}
/**
* Subtracts the commit/line stats of the given chunks from the named student.
*/
private void subtractChunkStatsFromStudent(TeamParticipation participation, String studentName,
List<AnalyzedChunk> removedChunks) {
ChunkStatsDelta delta = computeChunkStats(removedChunks);
if (delta.commits == 0 && delta.linesChanged == 0) {
return;
}
studentRepository.findAllByTeam(participation).stream()
.filter(s -> studentName.equals(s.getName()))
.findFirst()
.ifPresent(student -> {
student.setCommitCount(Math.max(0, safe(student.getCommitCount()) - delta.commits));
student.setLinesChanged(Math.max(0, safe(student.getLinesChanged()) - delta.linesChanged));
applyLinesSplit(student, delta.linesChanged, false);
studentRepository.save(student);
});
}
private record ChunkStatsDelta(int commits, int linesChanged) {}
private ChunkStatsDelta computeChunkStats(List<AnalyzedChunk> chunks) {
int totalCommits = 0;
int totalLines = 0;
for (AnalyzedChunk chunk : chunks) {
if (chunk.getCommitShas() != null && !chunk.getCommitShas().isEmpty()) {
totalCommits += chunk.getCommitShas().split(",").length;
}
totalLines += chunk.getLinesChanged() != null ? chunk.getLinesChanged() : 0;
}
return new ChunkStatsDelta(totalCommits, totalLines);
}
private void applyLinesSplit(Student student, int deltaLines, boolean add) {
CqiRecalculationService.applyLinesSplit(student, deltaLines, add);
}
private static int safe(Integer value) {
return value != null ? value : 0;
}
private static boolean isExternalChunkForEmail(AnalyzedChunk chunk, String normalizedEmail) {
return Boolean.TRUE.equals(chunk.getIsExternalContributor())
&& normalizedEmail.equals(chunk.getAuthorEmail() != null
? chunk.getAuthorEmail().toLowerCase(Locale.ROOT) : null);
}
/**
* Builds the set of emails that are considered "known" for a participation.
* Includes student emails and remaining exercise email mappings.
*/
private Set<String> buildKnownEmails(TeamParticipation participation, Long exerciseId) {
Set<String> known = new HashSet<>();
List<Student> students = studentRepository.findAllByTeam(participation);
for (Student s : students) {
if (s.getEmail() != null) {
known.add(s.getEmail().toLowerCase(Locale.ROOT));
}
}
for (ExerciseEmailMapping m : emailMappingRepository.findAllByExerciseId(exerciseId)) {
known.add(m.getGitEmail().toLowerCase(Locale.ROOT));
}
return known;
}
private ClientResponseDTO buildResponse(TeamParticipation participation) {
List<Student> students = studentRepository.findAllByTeam(participation);
List<StudentAnalysisDTO> studentDTOs = students.stream()
.map(s -> new StudentAnalysisDTO(s.getName(), s.getCommitCount(),
s.getLinesAdded(), s.getLinesDeleted(), s.getLinesChanged()))
.toList();
return new ClientResponseDTO(
participation.getTutor() != null ? participation.getTutor().getName() : "Unassigned",
participation.getTeam(),
participation.getParticipation(),
participation.getName(),
participation.getShortName(),
participation.getSubmissionCount(),
studentDTOs,
participation.getCqi(),
participation.getIsSuspicious(),
participation.getAnalysisStatus(),
null, // CQI details will be loaded by regular getData endpoint
loadAnalyzedChunkDTOs(participation),
null, // orphan commits not persisted
readTeamTokenTotals(participation),
participation.getOrphanCommitCount(),
participation.getIsFailed(),
participation.getIsReviewed());
}
private List<AnalyzedChunkDTO> loadAnalyzedChunkDTOs(TeamParticipation participation) {
List<AnalyzedChunk> chunks = analyzedChunkRepository.findByParticipation(participation);
if (chunks.isEmpty()) {
return null;
}
return chunks.stream()
.map(chunk -> new AnalyzedChunkDTO(
chunk.getChunkIdentifier(),
chunk.getAuthorEmail(),
chunk.getAuthorName(),
chunk.getClassification(),
chunk.getEffortScore() != null ? chunk.getEffortScore() : 0.0,
chunk.getComplexity() != null ? chunk.getComplexity() : 0.0,
chunk.getNovelty() != null ? chunk.getNovelty() : 0.0,
chunk.getConfidence() != null ? chunk.getConfidence() : 0.0,
chunk.getReasoning(),
List.of(chunk.getCommitShas().split(",")),
parseCommitMessages(chunk.getCommitMessages()),
chunk.getTimestamp(),
chunk.getLinesChanged() != null ? chunk.getLinesChanged() : 0,
Boolean.TRUE.equals(chunk.getIsBundled()),
chunk.getChunkIndex() != null ? chunk.getChunkIndex() : 0,
chunk.getTotalChunks() != null ? chunk.getTotalChunks() : 1,
Boolean.TRUE.equals(chunk.getIsError()),
chunk.getErrorMessage(),
Boolean.TRUE.equals(chunk.getIsExternalContributor()),
new LlmTokenUsageDTO(
chunk.getLlmModel() != null ? chunk.getLlmModel() : "unknown",
chunk.getLlmPromptTokens() != null ? chunk.getLlmPromptTokens() : 0L,
chunk.getLlmCompletionTokens() != null ? chunk.getLlmCompletionTokens() : 0L,
chunk.getLlmTotalTokens() != null
? chunk.getLlmTotalTokens()
: (chunk.getLlmPromptTokens() != null ? chunk.getLlmPromptTokens() : 0L)
+ (chunk.getLlmCompletionTokens() != null
? chunk.getLlmCompletionTokens() : 0L),
Boolean.TRUE.equals(chunk.getLlmUsageAvailable()))))
.toList();
}
private LlmTokenTotalsDTO readTeamTokenTotals(TeamParticipation p) {
if (p.getLlmCalls() == null) {
return null;
}
return new LlmTokenTotalsDTO(
p.getLlmCalls() != null ? p.getLlmCalls() : 0L,
p.getLlmCallsWithUsage() != null ? p.getLlmCallsWithUsage() : 0L,
p.getLlmPromptTokens() != null ? p.getLlmPromptTokens() : 0L,
p.getLlmCompletionTokens() != null ? p.getLlmCompletionTokens() : 0L,
p.getLlmTotalTokens() != null ? p.getLlmTotalTokens() : 0L);
}
@SuppressWarnings("unchecked")
private List<String> parseCommitMessages(String json) {
try {
if (json == null || json.isEmpty()) {
return List.of();
}
return OBJECT_MAPPER.readValue(json, List.class);
} catch (Exception e) {
return List.of();
}
}
}