Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ paths:
description: OK
content:
application/json:
schema:
type: array
items: {$ref: '#/components/schemas/ComplianceIssue'}
schema: {$ref: '#/components/schemas/JobAnalysisDTO'}
/api/ai/extractPdfData:
put:
tags: [ai-resource]
Expand Down Expand Up @@ -3334,6 +3332,20 @@ components:
id: {type: string}
language: {type: string}
text: {type: string}
ComplianceIssueDTO:
type: object
properties:
action:
type: string
enum: [REPLACE, ADD, REMOVE]
article: {type: string}
category:
type: string
enum: [CRITICAL_AGG, TRANSPARENCY, DSGVO_MINIMIZATION, PUBLIC_SECTOR]
explanation: {type: string}
id: {type: string}
language: {type: string}
text: {type: string}
ConflictDataDTO:
type: object
properties:
Expand Down Expand Up @@ -3636,6 +3648,13 @@ components:
firstName: {type: string}
lastName: {type: string}
userId: {type: string, format: uuid}
JobAnalysisDTO:
type: object
properties:
issues:
type: array
items: {$ref: '#/components/schemas/ComplianceIssueDTO'}
score: {type: integer, format: int32}
JobCardDTO:
type: object
properties:
Expand Down Expand Up @@ -4307,6 +4326,7 @@ components:
TranslateComplianceDTO:
type: object
properties:
jobId: {type: string, format: uuid}
originalAnalysis: {$ref: '#/components/schemas/GenderBiasAnalysisResponse'}
text: {type: string, minLength: 1}
required: [text]
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/de/tum/cit/aet/ai/dto/ComplianceIssueDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.tum.cit.aet.ai.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import de.tum.cit.aet.ai.constants.ComplianceAction;
import de.tum.cit.aet.ai.constants.ComplianceCategory;
import de.tum.cit.aet.ai.domain.ComplianceIssue;

/** Response DTO for one compliance issue. */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ComplianceIssueDTO(
String id,
ComplianceCategory category,
String text,
String article,
String explanation,
ComplianceAction action,
String language
) {
/**
* Creates an API response from the persisted compliance value.
*
* @param issue the persisted compliance issue
* @return the response DTO
*/
public static ComplianceIssueDTO from(ComplianceIssue issue) {
return new ComplianceIssueDTO(
issue.getId(),
issue.getCategory(),
issue.getText(),
issue.getArticle(),
issue.getExplanation(),
issue.getAction(),
issue.getLanguage()
);
}
}
20 changes: 20 additions & 0 deletions src/main/java/de/tum/cit/aet/ai/dto/JobAnalysisDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package de.tum.cit.aet.ai.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import de.tum.cit.aet.ai.domain.ComplianceIssue;
import java.util.List;

/** Response DTO for job-description analysis. */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record JobAnalysisDTO(int score, List<ComplianceIssueDTO> issues) {
/**
* Creates the response while keeping persistence models behind the DTO boundary.
*
* @param score the persisted combined score
* @param issues the detected compliance issues
* @return the analysis response
*/
public static JobAnalysisDTO from(int score, List<ComplianceIssue> issues) {
return new JobAnalysisDTO(score, issues.stream().map(ComplianceIssueDTO::from).toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.tum.cit.aet.core.dto.GenderBiasAnalysisResponse;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotBlank;
import java.util.UUID;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record TranslateComplianceDTO(@NotBlank String text, @Nullable GenderBiasAnalysisResponse originalAnalysis) {}
public record TranslateComplianceDTO(@NotBlank String text, @Nullable GenderBiasAnalysisResponse originalAnalysis, @Nullable UUID jobId) {}
72 changes: 72 additions & 0 deletions src/main/java/de/tum/cit/aet/ai/service/AiPriorityService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package de.tum.cit.aet.ai.service;

import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;

/** Coordinates foreground generation and cancellable background AI work per job. */
@Service
public class AiPriorityService {

private final Map<UUID, Set<Sinks.Empty<Void>>> backgroundCancellations = new ConcurrentHashMap<>();

/**
* Cancels existing background work for the job before generation starts.
*
* @param jobId the job owning the AI workflow
* @param source the generation stream
* @return the generation stream
* @param <T> the streamed response type
*/
public <T> Flux<T> foreground(UUID jobId, Flux<T> source) {
if (jobId == null) {
return source;
}
return Flux.defer(() -> {
cancelBackground(jobId);
return source;
});
}

/**
* Registers background work so foreground generation for the same job can cancel it.
*
* @param jobId the job owning the AI workflow
* @param source the background stream
* @return the cancellable background stream
* @param <T> the streamed response type
*/
public <T> Flux<T> background(UUID jobId, Flux<T> source) {
if (jobId == null) {
return source;
}
return Flux.defer(() -> {
Sinks.Empty<Void> cancellation = Sinks.empty();
backgroundCancellations.computeIfAbsent(jobId, _ -> ConcurrentHashMap.newKeySet()).add(cancellation);
Mono<T> cancellationError = cancellation
.asMono()
.then(Mono.error(new CancellationException("AI request superseded by generation")));
return source.takeUntilOther(cancellationError).doFinally(_ -> unregister(jobId, cancellation));
});
}

private void cancelBackground(UUID jobId) {
Set<Sinks.Empty<Void>> cancellations = backgroundCancellations.remove(jobId);
if (cancellations != null) {
cancellations.forEach(Sinks.Empty::tryEmitEmpty);
}
}

private void unregister(UUID jobId, Sinks.Empty<Void> cancellation) {
backgroundCancellations.computeIfPresent(jobId, (_, cancellations) -> {
cancellations.remove(cancellation);
return cancellations.isEmpty() ? null : cancellations;
});
}
}
Loading
Loading