Ajay Yadav commited on
Commit
86a3cc6
·
1 Parent(s): 5311988

Initial deployment of da-autolabel-dev

Browse files
Files changed (46) hide show
  1. Dockerfile +27 -0
  2. README.md +34 -6
  3. build.gradle.kts +23 -0
  4. src/main/docker/Dockerfile +23 -0
  5. src/main/docker/Dockerfile.alpine-jlink +43 -0
  6. src/main/docker/Dockerfile.layered +34 -0
  7. src/main/docker/Dockerfile.native +20 -0
  8. src/main/java/com/dalab/autolabel/DaAutolabelApplication.java +34 -0
  9. src/main/java/com/dalab/autolabel/client/dto/AssetIdentifier.java +12 -0
  10. src/main/java/com/dalab/autolabel/client/feign/AssetCatalogClient.java +39 -0
  11. src/main/java/com/dalab/autolabel/client/rest/dto/LabelingFeedbackRequest.java +64 -0
  12. src/main/java/com/dalab/autolabel/client/rest/dto/LabelingFeedbackResponse.java +30 -0
  13. src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobListResponse.java +47 -0
  14. src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobRequest.java +82 -0
  15. src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobResponse.java +83 -0
  16. src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobStatusResponse.java +49 -0
  17. src/main/java/com/dalab/autolabel/client/rest/dto/MLConfigRequest.java +108 -0
  18. src/main/java/com/dalab/autolabel/controller/AutoLabelingController.java +239 -0
  19. src/main/java/com/dalab/autolabel/controller/LabelingConfigController.java +74 -0
  20. src/main/java/com/dalab/autolabel/controller/LabelingJobController.java +116 -0
  21. src/main/java/com/dalab/autolabel/entity/LabelingFeedbackEntity.java +52 -0
  22. src/main/java/com/dalab/autolabel/entity/LabelingJobEntity.java +72 -0
  23. src/main/java/com/dalab/autolabel/exception/AutoLabelingException.java +17 -0
  24. src/main/java/com/dalab/autolabel/llm/client/ILLMClient.java +75 -0
  25. src/main/java/com/dalab/autolabel/llm/client/LLMClientFactory.java +137 -0
  26. src/main/java/com/dalab/autolabel/llm/client/impl/MockLLMClient.java +138 -0
  27. src/main/java/com/dalab/autolabel/llm/client/impl/OllamaLLMClient.java +355 -0
  28. src/main/java/com/dalab/autolabel/llm/client/impl/OpenAiLLMClient.java +328 -0
  29. src/main/java/com/dalab/autolabel/llm/model/LLMResponse.java +80 -0
  30. src/main/java/com/dalab/autolabel/llm/model/LabelSuggestion.java +36 -0
  31. src/main/java/com/dalab/autolabel/mapper/LabelingJobMapper.java +38 -0
  32. src/main/java/com/dalab/autolabel/repository/LabelingFeedbackRepository.java +19 -0
  33. src/main/java/com/dalab/autolabel/repository/LabelingJobRepository.java +13 -0
  34. src/main/java/com/dalab/autolabel/security/SecurityUtils.java +1 -0
  35. src/main/java/com/dalab/autolabel/service/AutoLabelingService.java +395 -0
  36. src/main/java/com/dalab/autolabel/service/ILLMIntegrationService.java +21 -0
  37. src/main/java/com/dalab/autolabel/service/ILabelingJobService.java +52 -0
  38. src/main/java/com/dalab/autolabel/service/IMLConfigService.java +24 -0
  39. src/main/java/com/dalab/autolabel/service/impl/InMemoryMLConfigService.java +53 -0
  40. src/main/java/com/dalab/autolabel/service/impl/LLMIntegrationServiceImpl.java +69 -0
  41. src/main/java/com/dalab/autolabel/service/impl/LabelingJobServiceImpl.java +280 -0
  42. src/main/resources/application.properties +85 -0
  43. src/test/java/com/dalab/autolabel/controller/LabelingConfigControllerTest.java +94 -0
  44. src/test/java/com/dalab/autolabel/controller/LabelingJobControllerTest.java +173 -0
  45. src/test/java/com/dalab/autolabel/service/impl/InMemoryMLConfigServiceTest.java +59 -0
  46. src/test/java/com/dalab/autolabel/service/impl/LabelingJobServiceImplTest.java +179 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM openjdk:21-jdk-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install required packages
6
+ RUN apt-get update && apt-get install -y \
7
+ curl \
8
+ wget \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy application files
12
+ COPY . .
13
+
14
+ # Build application (if build.gradle.kts exists)
15
+ RUN if [ -f "build.gradle.kts" ]; then \
16
+ ./gradlew build -x test; \
17
+ fi
18
+
19
+ # Expose port
20
+ EXPOSE 8080
21
+
22
+ # Health check
23
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
24
+ CMD curl -f http://localhost:8080/actuator/health || exit 1
25
+
26
+ # Run application
27
+ CMD ["java", "-jar", "build/libs/da-autolabel.jar"]
README.md CHANGED
@@ -1,10 +1,38 @@
1
  ---
2
- title: Da Autolabel Dev
3
- emoji:
4
- colorFrom: purple
5
- colorTo: yellow
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: da-autolabel (dev)
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 8080
8
  ---
9
 
10
+ # da-autolabel - dev Environment
11
+
12
+ This is the da-autolabel microservice deployed in the dev environment.
13
+
14
+ ## Features
15
+
16
+ - RESTful API endpoints
17
+ - Health monitoring via Actuator
18
+ - JWT authentication integration
19
+ - PostgreSQL database connectivity
20
+
21
+ ## API Documentation
22
+
23
+ Once deployed, API documentation will be available at:
24
+ - Swagger UI: https://huggingface.co/spaces/dalabsai/da-autolabel-dev/swagger-ui.html
25
+ - Health Check: https://huggingface.co/spaces/dalabsai/da-autolabel-dev/actuator/health
26
+
27
+ ## Environment
28
+
29
+ - **Environment**: dev
30
+ - **Port**: 8080
31
+ - **Java Version**: 21
32
+ - **Framework**: Spring Boot
33
+
34
+ ## Deployment
35
+
36
+ This service is automatically deployed via the DALab CI/CD pipeline.
37
+
38
+ Last updated: 2025-06-16 23:40:06
build.gradle.kts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // da-autolabel inherits common configuration from parent build.gradle.kts
2
+ // This build file adds autolabel-specific dependencies
3
+
4
+ dependencies {
5
+ // DA-Protos common entities and utilities
6
+ implementation(project(":da-protos"))
7
+
8
+ // LLM Integration Dependencies
9
+ implementation("com.theokanning.openai-gpt3-java:service:0.18.2")
10
+ implementation("com.google.cloud:google-cloud-aiplatform:3.34.0")
11
+ implementation("org.springframework.boot:spring-boot-starter-webflux")
12
+ implementation("org.apache.httpcomponents.client5:httpclient5:5.2.1")
13
+ implementation("com.fasterxml.jackson.core:jackson-databind")
14
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
15
+
16
+ // Additional dependencies specific to da-autolabel
17
+ implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
18
+ }
19
+
20
+ // Configure main application class
21
+ configure<org.springframework.boot.gradle.dsl.SpringBootExtension> {
22
+ mainClass.set("com.dalab.autolabel.DaAutolabelApplication")
23
+ }
src/main/docker/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-lean container using Google Distroless
2
+ # Expected final size: ~120-180MB (minimal base + JRE + JAR only)
3
+
4
+ FROM gcr.io/distroless/java21-debian12:nonroot
5
+
6
+ # Set working directory
7
+ WORKDIR /app
8
+
9
+ # Copy JAR file
10
+ COPY build/libs/da-autolabel.jar app.jar
11
+
12
+ # Expose standard Spring Boot port
13
+ EXPOSE 8080
14
+
15
+ # Run application (distroless has no shell, so use exec form)
16
+ ENTRYPOINT ["java", \
17
+ "-XX:+UseContainerSupport", \
18
+ "-XX:MaxRAMPercentage=75.0", \
19
+ "-XX:+UseG1GC", \
20
+ "-XX:+UseStringDeduplication", \
21
+ "-Djava.security.egd=file:/dev/./urandom", \
22
+ "-Dspring.backgroundpreinitializer.ignore=true", \
23
+ "-jar", "app.jar"]
src/main/docker/Dockerfile.alpine-jlink ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-minimal Alpine + Custom JRE
2
+ # Expected size: ~120-160MB
3
+
4
+ # Stage 1: Create custom JRE with only needed modules
5
+ FROM eclipse-temurin:21-jdk-alpine as jre-builder
6
+ WORKDIR /app
7
+
8
+ # Analyze JAR to find required modules
9
+ COPY build/libs/*.jar app.jar
10
+ RUN jdeps --ignore-missing-deps --print-module-deps app.jar > modules.txt
11
+
12
+ # Create minimal JRE with only required modules
13
+ RUN jlink \
14
+ --add-modules $(cat modules.txt),java.logging,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
15
+ --strip-debug \
16
+ --no-man-pages \
17
+ --no-header-files \
18
+ --compress=2 \
19
+ --output /custom-jre
20
+
21
+ # Stage 2: Production image
22
+ FROM alpine:3.19
23
+ RUN apk add --no-cache tzdata && \
24
+ addgroup -g 1001 -S appgroup && \
25
+ adduser -u 1001 -S appuser -G appgroup
26
+
27
+ # Copy custom JRE
28
+ COPY --from=jre-builder /custom-jre /opt/java
29
+ ENV JAVA_HOME=/opt/java
30
+ ENV PATH="$JAVA_HOME/bin:$PATH"
31
+
32
+ WORKDIR /app
33
+ COPY build/libs/*.jar app.jar
34
+ RUN chown appuser:appgroup app.jar
35
+
36
+ USER appuser
37
+ EXPOSE 8080
38
+
39
+ ENTRYPOINT ["java", \
40
+ "-XX:+UseContainerSupport", \
41
+ "-XX:MaxRAMPercentage=70.0", \
42
+ "-XX:+UseG1GC", \
43
+ "-jar", "app.jar"]
src/main/docker/Dockerfile.layered ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-optimized layered build using Distroless
2
+ # Expected size: ~180-220MB with better caching
3
+
4
+ FROM gcr.io/distroless/java21-debian12:nonroot as base
5
+
6
+ # Stage 1: Extract JAR layers for optimal caching
7
+ FROM eclipse-temurin:21-jdk-alpine as extractor
8
+ WORKDIR /app
9
+ COPY build/libs/*.jar app.jar
10
+ RUN java -Djarmode=layertools -jar app.jar extract
11
+
12
+ # Stage 2: Production image with extracted layers
13
+ FROM base
14
+ WORKDIR /app
15
+
16
+ # Copy layers in dependency order (best caching)
17
+ COPY --from=extractor /app/dependencies/ ./
18
+ COPY --from=extractor /app/spring-boot-loader/ ./
19
+ COPY --from=extractor /app/snapshot-dependencies/ ./
20
+ COPY --from=extractor /app/application/ ./
21
+
22
+ EXPOSE 8080
23
+
24
+ # Optimized JVM settings for micro-containers
25
+ ENTRYPOINT ["java", \
26
+ "-XX:+UseContainerSupport", \
27
+ "-XX:MaxRAMPercentage=70.0", \
28
+ "-XX:+UseG1GC", \
29
+ "-XX:+UseStringDeduplication", \
30
+ "-XX:+CompactStrings", \
31
+ "-Xshare:on", \
32
+ "-Djava.security.egd=file:/dev/./urandom", \
33
+ "-Dspring.backgroundpreinitializer.ignore=true", \
34
+ "org.springframework.boot.loader.JarLauncher"]
src/main/docker/Dockerfile.native ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GraalVM Native Image - Ultra-fast startup, tiny size
2
+ # Expected size: ~50-80MB, startup <100ms
3
+ # Note: Requires native compilation support in Spring Boot
4
+
5
+ # Stage 1: Native compilation
6
+ FROM ghcr.io/graalvm/graalvm-ce:ol9-java21 as native-builder
7
+ WORKDIR /app
8
+
9
+ # Install native-image
10
+ RUN gu install native-image
11
+
12
+ # Copy source and build native executable
13
+ COPY . .
14
+ RUN ./gradlew nativeCompile
15
+
16
+ # Stage 2: Minimal runtime
17
+ FROM scratch
18
+ COPY --from=native-builder /app/build/native/nativeCompile/app /app
19
+ EXPOSE 8080
20
+ ENTRYPOINT ["/app"]
src/main/java/com/dalab/autolabel/DaAutolabelApplication.java ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel;
2
+
3
+ import io.swagger.v3.oas.models.OpenAPI;
4
+ import io.swagger.v3.oas.models.info.Info;
5
+ import io.swagger.v3.oas.models.info.License;
6
+ import org.springframework.beans.factory.annotation.Value;
7
+ import org.springframework.boot.SpringApplication;
8
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
9
+ import org.springframework.context.annotation.Bean;
10
+ import org.springframework.scheduling.annotation.EnableAsync;
11
+ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
12
+
13
+ @SpringBootApplication
14
+ @EnableMethodSecurity // Enable method-level security (for @PreAuthorize)
15
+ @EnableAsync // Enable asynchronous method execution
16
+ public class DaAutolabelApplication {
17
+
18
+ public static void main(String[] args) {
19
+ SpringApplication.run(DaAutolabelApplication.class, args);
20
+ }
21
+
22
+ @Bean
23
+ public OpenAPI customOpenAPI(@Value("${spring.application.name:DALab AutoLabel Service}") String appName,
24
+ @Value("${spring.application.description:API for AutoLabel Service}") String appDescription,
25
+ @Value("${spring.application.version:0.0.1-SNAPSHOT}") String appVersion) {
26
+ return new OpenAPI()
27
+ .info(new Info()
28
+ .title(appName)
29
+ .version(appVersion)
30
+ .description(appDescription)
31
+ .termsOfService("http://swagger.io/terms/") // Placeholder
32
+ .license(new License().name("Apache 2.0").url("http://springdoc.org"))); // Placeholder
33
+ }
34
+ }
src/main/java/com/dalab/autolabel/client/dto/AssetIdentifier.java ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.dto;
2
+
3
+ import lombok.Builder;
4
+ import lombok.Data;
5
+
6
+ @Data
7
+ @Builder
8
+ public class AssetIdentifier {
9
+ private String assetId;
10
+ private String provider; // Optional: e.g., GCP, AWS, AZURE, OCI, for context
11
+ // Add other relevant fields that might be needed from catalog to identify/fetch asset details
12
+ }
src/main/java/com/dalab/autolabel/client/feign/AssetCatalogClient.java ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.feign;
2
+
3
+ import com.dalab.autolabel.client.dto.AssetIdentifier; // Assuming a DTO from da-catalog
4
+ import com.dalab.autolabel.client.rest.dto.LabelingJobRequest;
5
+ import org.springframework.cloud.openfeign.FeignClient;
6
+ import org.springframework.web.bind.annotation.DeleteMapping;
7
+ import org.springframework.web.bind.annotation.GetMapping;
8
+ import org.springframework.web.bind.annotation.PathVariable;
9
+ import org.springframework.web.bind.annotation.PostMapping;
10
+ import org.springframework.web.bind.annotation.RequestBody;
11
+ import org.springframework.web.bind.annotation.RequestParam;
12
+
13
+ import java.util.List;
14
+ import java.util.Map;
15
+
16
+ @FeignClient(name = "da-catalog", path = "/api/v1/catalog") // Value from da-catalog application properties
17
+ public interface AssetCatalogClient {
18
+
19
+ // Example: Get assets by connection ID (actual endpoint TBD in da-catalog)
20
+ @GetMapping("/assets")
21
+ List<AssetIdentifier> getAssetsByConnection(@RequestParam("connectionId") String connectionId);
22
+
23
+ // Example: Get assets by criteria (actual endpoint TBD in da-catalog)
24
+ @PostMapping("/assets/search") // Assuming a search endpoint
25
+ List<AssetIdentifier> findAssetsByCriteria(@RequestBody Map<String, String> criteria);
26
+
27
+ // Example: Get minimal metadata for an asset (actual endpoint TBD in da-catalog)
28
+ @GetMapping("/assets/{assetId}/metadata/technical") // Or a specific slimmed down DTO endpoint
29
+ String getAssetTechnicalMetadata(@PathVariable("assetId") String assetId);
30
+
31
+ // Example: Add labels to an asset (actual endpoint TBD in da-catalog)
32
+ @PostMapping("/assets/{assetId}/labels")
33
+ void addLabelsToAsset(@PathVariable("assetId") String assetId, @RequestBody List<String> labels);
34
+
35
+ // Example: Remove a label from an asset (actual endpoint TBD in da-catalog)
36
+ @DeleteMapping("/assets/{assetId}/labels/{labelName}")
37
+ void removeLabelFromAsset(@PathVariable("assetId") String assetId, @PathVariable("labelName") String labelName);
38
+
39
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/LabelingFeedbackRequest.java ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.util.List;
4
+ import java.util.Map;
5
+
6
+ import jakarta.validation.constraints.NotBlank;
7
+ import jakarta.validation.constraints.NotEmpty;
8
+ import jakarta.validation.constraints.NotNull;
9
+ import lombok.Data;
10
+ import lombok.NoArgsConstructor;
11
+ import lombok.AllArgsConstructor;
12
+ import lombok.Builder;
13
+
14
+ /**
15
+ * Request DTO for submitting feedback on auto-suggested labels.
16
+ */
17
+ @Data
18
+ @NoArgsConstructor
19
+ @AllArgsConstructor
20
+ @Builder
21
+ public class LabelingFeedbackRequest {
22
+
23
+ @NotBlank(message = "Asset ID cannot be blank")
24
+ private String assetId;
25
+
26
+ @NotBlank(message = "Job ID that generated the suggestions cannot be blank")
27
+ private String labelingJobId; // ID of the job that produced these suggestions
28
+
29
+ @NotEmpty(message = "Feedback items cannot be empty")
30
+ private List<FeedbackItem> feedbackItems;
31
+
32
+ private String userId; // Optional: User providing the feedback
33
+ private Map<String, String> additionalContext; // Optional: e.g., UI version, session ID
34
+
35
+ /**
36
+ * Nested DTO for individual feedback items.
37
+ */
38
+ @Data
39
+ @NoArgsConstructor
40
+ @AllArgsConstructor
41
+ @Builder
42
+ public static class FeedbackItem {
43
+ @NotBlank(message = "Suggested label cannot be blank")
44
+ private String suggestedLabel;
45
+
46
+ private Double originalConfidence; // Optional: Confidence score from the LLM
47
+
48
+ @NotNull(message = "Feedback type cannot be null")
49
+ private FeedbackType type; // e.g., CONFIRMED, REJECTED, CORRECTED
50
+
51
+ private String correctedLabel; // If type is CORRECTED
52
+ private String userComment; // Optional comment
53
+ }
54
+
55
+ /**
56
+ * Enum for feedback type.
57
+ */
58
+ public enum FeedbackType {
59
+ CONFIRMED, // User agrees with the suggestion
60
+ REJECTED, // User disagrees with the suggestion
61
+ CORRECTED, // User provides a corrected label
62
+ FLAGGED // User flags for further review, without immediate correction
63
+ }
64
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/LabelingFeedbackResponse.java ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.time.LocalDateTime;
4
+
5
+ import lombok.AllArgsConstructor;
6
+ import lombok.Builder;
7
+ import lombok.Data;
8
+ import lombok.NoArgsConstructor;
9
+
10
+ /**
11
+ * Response DTO after submitting labeling feedback.
12
+ */
13
+ @Data
14
+ @NoArgsConstructor
15
+ @AllArgsConstructor
16
+ @Builder
17
+ public class LabelingFeedbackResponse {
18
+
19
+ private String feedbackId; // A unique ID for this feedback submission
20
+ private String assetId;
21
+ private String status; // e.g., "RECEIVED", "PROCESSED", "ERROR"
22
+ private String message;
23
+ private LocalDateTime receivedAt;
24
+
25
+ /**
26
+ * Timestamp when the feedback was processed.
27
+ */
28
+ private LocalDateTime processedAt;
29
+
30
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobListResponse.java ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.util.List;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+ import lombok.AllArgsConstructor;
7
+ import lombok.Builder;
8
+
9
+ /**
10
+ * Response DTO for listing auto-labeling jobs with pagination information.
11
+ */
12
+ @Data
13
+ @NoArgsConstructor
14
+ @AllArgsConstructor
15
+ @Builder
16
+ public class LabelingJobListResponse {
17
+
18
+ /**
19
+ * List of job statuses for the current page.
20
+ */
21
+ private List<LabelingJobStatusResponse> jobs;
22
+
23
+ /**
24
+ * Current page number (0-indexed).
25
+ */
26
+ private int pageNumber;
27
+
28
+ /**
29
+ * Number of jobs per page.
30
+ */
31
+ private int pageSize;
32
+
33
+ /**
34
+ * Total number of jobs matching the criteria.
35
+ */
36
+ private long totalElements;
37
+
38
+ /**
39
+ * Total number of pages.
40
+ */
41
+ private int totalPages;
42
+
43
+ /**
44
+ * Indicates if this is the last page.
45
+ */
46
+ private boolean last;
47
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobRequest.java ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.util.List;
4
+ import java.util.Map;
5
+
6
+ import jakarta.validation.Valid;
7
+ import jakarta.validation.constraints.NotNull;
8
+ import lombok.Data;
9
+ import lombok.NoArgsConstructor;
10
+ import lombok.AllArgsConstructor;
11
+ import lombok.Builder;
12
+
13
+ /**
14
+ * Request DTO to trigger an auto-labeling job.
15
+ */
16
+ @Data
17
+ @NoArgsConstructor
18
+ @AllArgsConstructor
19
+ @Builder
20
+ public class LabelingJobRequest {
21
+
22
+ /**
23
+ * A user-defined name for this labeling job for easier identification.
24
+ */
25
+ @NotNull(message = "Job name cannot be null")
26
+ private String jobName;
27
+
28
+ /**
29
+ * Specifies the scope of assets to be labeled.
30
+ * Only one of these should typically be provided.
31
+ */
32
+ @NotNull(message = "Labeling scope cannot be null")
33
+ @Valid // Enable validation for nested LabelingScope object
34
+ private LabelingScope scope;
35
+
36
+ /**
37
+ * Optional: Override ML configuration for this specific job.
38
+ * If not provided, the globally configured ML settings will be used.
39
+ */
40
+ @Valid // Enable validation for nested MLConfigRequest object if annotations are added there
41
+ private MLConfigRequest overrideMlConfig;
42
+
43
+ /**
44
+ * Optional: Parameters to control job execution, e.g., priority, run mode (dry-run vs apply).
45
+ */
46
+ private Map<String, String> jobParameters;
47
+
48
+ /**
49
+ * Nested DTO for defining the scope of the labeling job.
50
+ */
51
+ @Data
52
+ @NoArgsConstructor
53
+ @AllArgsConstructor
54
+ @Builder
55
+ public static class LabelingScope {
56
+ /**
57
+ * List of specific asset IDs to label.
58
+ */
59
+ private List<String> assetIds;
60
+
61
+ /**
62
+ * ID of a cloud connection. All assets under this connection will be considered.
63
+ * This implies da-autolabel might need to query da-catalog for assets based on connectionId.
64
+ */
65
+ private String cloudConnectionId;
66
+
67
+ /**
68
+ * Criteria for selecting assets, e.g., based on existing labels, asset types, or other metadata.
69
+ * This would be a more complex filter.
70
+ * For now, represented as a map, but could be a more structured query object.
71
+ */
72
+ private Map<String, String> assetCriteria;
73
+
74
+ /**
75
+ * An identifier for a pre-defined asset group or collection.
76
+ */
77
+ private String assetGroupId;
78
+
79
+ // It would be good to add a validation ensuring at least one scope field is provided.
80
+ // This can be done with a custom class-level validator, or by checking in the service layer.
81
+ }
82
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobResponse.java ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.List;
5
+
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+ import lombok.AllArgsConstructor;
9
+ import lombok.Builder;
10
+
11
+ /**
12
+ * Response DTO after submitting an auto-labeling job.
13
+ */
14
+ @Data
15
+ @NoArgsConstructor
16
+ @AllArgsConstructor
17
+ @Builder
18
+ public class LabelingJobResponse {
19
+
20
+ /**
21
+ * The unique identifier for the submitted labeling job.
22
+ */
23
+ private String jobId;
24
+
25
+ /**
26
+ * The asset ID that was processed (for single asset jobs).
27
+ */
28
+ private String assetId;
29
+
30
+ /**
31
+ * The current status of the job (e.g., "SUBMITTED", "RUNNING", "COMPLETED", "FAILED").
32
+ */
33
+ private String status;
34
+
35
+ /**
36
+ * Timestamp when the job was submitted/created.
37
+ */
38
+ private LocalDateTime submittedAt;
39
+
40
+ /**
41
+ * Timestamp when the job was created.
42
+ */
43
+ private LocalDateTime createdAt;
44
+
45
+ /**
46
+ * Timestamp when the job was completed.
47
+ */
48
+ private LocalDateTime completedAt;
49
+
50
+ /**
51
+ * Processing time in milliseconds.
52
+ */
53
+ private Long processingTimeMs;
54
+
55
+ /**
56
+ * A brief message providing more details about the job submission.
57
+ */
58
+ private String message;
59
+
60
+ /**
61
+ * Error message if the job failed.
62
+ */
63
+ private String errorMessage;
64
+
65
+ /**
66
+ * List of label suggestions with confidence scores.
67
+ */
68
+ private List<LabelSuggestionDTO> suggestions;
69
+
70
+ /**
71
+ * DTO for label suggestions.
72
+ */
73
+ @Data
74
+ @NoArgsConstructor
75
+ @AllArgsConstructor
76
+ @Builder
77
+ public static class LabelSuggestionDTO {
78
+ private String labelName;
79
+ private Double confidence;
80
+ private String reasoning;
81
+ private String category;
82
+ }
83
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobStatusResponse.java ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.List;
5
+ import java.util.Map;
6
+
7
+ import lombok.Data;
8
+ import lombok.NoArgsConstructor;
9
+ import lombok.AllArgsConstructor;
10
+ import lombok.Builder;
11
+
12
+ /**
13
+ * Response DTO for querying the status of an auto-labeling job.
14
+ */
15
+ @Data
16
+ @NoArgsConstructor
17
+ @AllArgsConstructor
18
+ @Builder
19
+ public class LabelingJobStatusResponse {
20
+
21
+ private String jobId;
22
+ private String jobName; // From the original request
23
+ private String status; // e.g., SUBMITTED, PREPARING_DATA, CALLING_LLM, APPLYING_LABELS, COMPLETED, FAILED, PARTIALLY_COMPLETED
24
+ private LocalDateTime submittedAt;
25
+ private LocalDateTime startedAt;
26
+ private LocalDateTime lastUpdatedAt;
27
+ private LocalDateTime completedAt;
28
+
29
+ private Integer totalAssetsToProcess;
30
+ private Integer assetsProcessed;
31
+ private Integer assetsSuccessfullyLabeled;
32
+ private Integer assetsFailed;
33
+
34
+ private String currentStageDescription; // e.g., "Fetching metadata for asset X", "Calling LLM for batch Y"
35
+ private Double progressPercentage; // Overall progress
36
+
37
+ private List<String> errorMessages; // List of errors if any occurred
38
+
39
+ /**
40
+ * Optional: Link to where detailed results or logs might be found, if applicable.
41
+ */
42
+ private String resultsLink;
43
+
44
+ /**
45
+ * Optional: summary of labels applied or suggested if the job is complete or partially complete.
46
+ * Key: Label Name, Value: Count of assets this label was applied to.
47
+ */
48
+ private Map<String, Integer> labelSummary;
49
+ }
src/main/java/com/dalab/autolabel/client/rest/dto/MLConfigRequest.java ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.client.rest.dto;
2
+
3
+ import java.util.Map;
4
+
5
+ import lombok.Data;
6
+ import lombok.NoArgsConstructor;
7
+ import lombok.AllArgsConstructor;
8
+ import lombok.Builder;
9
+
10
+ /**
11
+ * Represents the request to configure Machine Learning (ML) settings for auto-labeling.
12
+ * This DTO is designed to be flexible to accommodate various LLM providers and their configurations.
13
+ */
14
+ @Data
15
+ @NoArgsConstructor
16
+ @AllArgsConstructor
17
+ @Builder
18
+ public class MLConfigRequest {
19
+
20
+ /**
21
+ * Specifies the ML provider to be used.
22
+ * Examples: "openai", "ollama", "gemini", "azure_openai", "vertex_ai".
23
+ */
24
+ private String providerType;
25
+
26
+ /**
27
+ * The API key for the selected ML provider, if applicable.
28
+ * This should be stored securely.
29
+ */
30
+ private String apiKey;
31
+
32
+ /**
33
+ * The base URL for the ML provider's API.
34
+ * Especially relevant for self-hosted models like Ollama or enterprise Azure OpenAI endpoints.
35
+ */
36
+ private String baseUrl;
37
+
38
+ /**
39
+ * The specific model name to be used from the provider.
40
+ * Examples: "gpt-3.5-turbo", "llama2", "gemini-pro".
41
+ */
42
+ private String modelName;
43
+
44
+ /**
45
+ * The prompt template to be used for generating labels.
46
+ * This template can include placeholders for metadata fields.
47
+ * Example: "Based on the following metadata, suggest relevant labels: {metadata_summary}"
48
+ */
49
+ private String promptTemplate;
50
+
51
+ /**
52
+ * A map for any additional provider-specific parameters.
53
+ * This allows for extending configuration without changing the DTO structure.
54
+ * Keys could be "temperature", "maxTokens", "deploymentName" (for Azure OpenAI), etc.
55
+ */
56
+ private Map<String, Object> additionalParameters;
57
+
58
+ /**
59
+ * Configuration for how many labels to suggest.
60
+ */
61
+ private SuggestionConfig suggestionConfig;
62
+
63
+ /**
64
+ * Configuration for confidence thresholds.
65
+ */
66
+ private ConfidenceConfig confidenceConfig;
67
+
68
+
69
+ /**
70
+ * Nested DTO for label suggestion configuration.
71
+ */
72
+ @Data
73
+ @NoArgsConstructor
74
+ @AllArgsConstructor
75
+ @Builder
76
+ public static class SuggestionConfig {
77
+ /**
78
+ * Maximum number of labels to suggest.
79
+ */
80
+ private Integer maxSuggestions;
81
+
82
+ /**
83
+ * Whether to include confidence scores with suggestions.
84
+ */
85
+ private Boolean includeConfidenceScores;
86
+ }
87
+
88
+ /**
89
+ * Nested DTO for confidence threshold configuration.
90
+ */
91
+ @Data
92
+ @NoArgsConstructor
93
+ @AllArgsConstructor
94
+ @Builder
95
+ public static class ConfidenceConfig {
96
+ /**
97
+ * Minimum confidence score for a suggestion to be considered.
98
+ * (Value between 0.0 and 1.0)
99
+ */
100
+ private Double minThreshold;
101
+
102
+ /**
103
+ * Action to take if confidence is below the minimum threshold.
104
+ * Examples: "REJECT", "FLAG_FOR_REVIEW"
105
+ */
106
+ private String actionBelowThreshold;
107
+ }
108
+ }
src/main/java/com/dalab/autolabel/controller/AutoLabelingController.java ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.controller;
2
+
3
+ import java.util.Map;
4
+ import java.util.UUID;
5
+
6
+ import org.slf4j.Logger;
7
+ import org.slf4j.LoggerFactory;
8
+ import org.springframework.beans.factory.annotation.Autowired;
9
+ import org.springframework.data.domain.Pageable;
10
+ import org.springframework.data.web.PageableDefault;
11
+ import org.springframework.http.HttpStatus;
12
+ import org.springframework.http.ResponseEntity;
13
+ import org.springframework.security.access.prepost.PreAuthorize;
14
+ import org.springframework.web.bind.annotation.GetMapping;
15
+ import org.springframework.web.bind.annotation.PathVariable;
16
+ import org.springframework.web.bind.annotation.PostMapping;
17
+ import org.springframework.web.bind.annotation.PutMapping;
18
+ import org.springframework.web.bind.annotation.RequestBody;
19
+ import org.springframework.web.bind.annotation.RequestMapping;
20
+ import org.springframework.web.bind.annotation.RequestParam;
21
+ import org.springframework.web.bind.annotation.RestController;
22
+
23
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackRequest;
24
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackResponse;
25
+ import com.dalab.autolabel.client.rest.dto.LabelingJobListResponse;
26
+ import com.dalab.autolabel.client.rest.dto.LabelingJobRequest;
27
+ import com.dalab.autolabel.client.rest.dto.LabelingJobResponse;
28
+ import com.dalab.autolabel.client.rest.dto.LabelingJobStatusResponse;
29
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
30
+ import com.dalab.autolabel.llm.client.LLMClientFactory;
31
+ import com.dalab.autolabel.service.AutoLabelingService;
32
+
33
+ import io.swagger.v3.oas.annotations.Operation;
34
+ import io.swagger.v3.oas.annotations.Parameter;
35
+ import io.swagger.v3.oas.annotations.tags.Tag;
36
+ import jakarta.validation.Valid;
37
+
38
+ /**
39
+ * REST controller for auto-labeling operations using LLM providers.
40
+ * Provides endpoints for job management, ML configuration, and feedback processing.
41
+ */
42
+ @RestController
43
+ @RequestMapping("/api/v1/autolabel")
44
+ @Tag(name = "Auto-Labeling", description = "LLM-powered auto-labeling operations")
45
+ public class AutoLabelingController {
46
+
47
+ private static final Logger log = LoggerFactory.getLogger(AutoLabelingController.class);
48
+
49
+ private final AutoLabelingService autoLabelingService;
50
+ private final LLMClientFactory llmClientFactory;
51
+
52
+ @Autowired
53
+ public AutoLabelingController(AutoLabelingService autoLabelingService, LLMClientFactory llmClientFactory) {
54
+ this.autoLabelingService = autoLabelingService;
55
+ this.llmClientFactory = llmClientFactory;
56
+ }
57
+
58
+ @PostMapping("/labeling/jobs")
59
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD', 'ROLE_USER')")
60
+ @Operation(summary = "Create a labeling job", description = "Creates a new auto-labeling job for the specified assets")
61
+ public ResponseEntity<LabelingJobResponse> createLabelingJob(
62
+ @Valid @RequestBody LabelingJobRequest request) {
63
+ log.info("REST request to create labeling job: {}", request.getJobName());
64
+
65
+ try {
66
+ LabelingJobResponse response = autoLabelingService.createLabelingJob(request);
67
+ return ResponseEntity.status(HttpStatus.ACCEPTED).body(response);
68
+ } catch (Exception e) {
69
+ log.error("Failed to create labeling job: {}", e.getMessage(), e);
70
+ LabelingJobResponse errorResponse = LabelingJobResponse.builder()
71
+ .jobId(UUID.randomUUID().toString())
72
+ .status("FAILED")
73
+ .message("Failed to create labeling job: " + e.getMessage())
74
+ .build();
75
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
76
+ }
77
+ }
78
+
79
+ @GetMapping("/labeling/jobs")
80
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD', 'ROLE_USER')")
81
+ @Operation(summary = "Get labeling jobs", description = "Retrieves a paginated list of labeling jobs")
82
+ public ResponseEntity<LabelingJobListResponse> getLabelingJobs(
83
+ @PageableDefault(size = 20, sort = "submittedAt") Pageable pageable,
84
+ @RequestParam(required = false) @Parameter(description = "Filter by job status") String status,
85
+ @RequestParam(required = false) @Parameter(description = "Filter by user ID") UUID userId) {
86
+ log.info("REST request to get labeling jobs with filters: status={}, userId={}", status, userId);
87
+
88
+ // TODO: Implement actual job listing from database
89
+ LabelingJobListResponse response = LabelingJobListResponse.builder()
90
+ .jobs(java.util.Collections.emptyList())
91
+ .totalElements(0L)
92
+ .pageNumber(pageable.getPageNumber())
93
+ .pageSize(pageable.getPageSize())
94
+ .totalPages(0)
95
+ .last(true)
96
+ .build();
97
+
98
+ return ResponseEntity.ok(response);
99
+ }
100
+
101
+ @GetMapping("/labeling/jobs/{jobId}")
102
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD', 'ROLE_USER')")
103
+ @Operation(summary = "Get job status", description = "Retrieves the status and details of a specific labeling job")
104
+ public ResponseEntity<LabelingJobStatusResponse> getJobStatus(@PathVariable String jobId) {
105
+ log.info("REST request to get status for job: {}", jobId);
106
+
107
+ try {
108
+ UUID jobUuid = UUID.fromString(jobId);
109
+ LabelingJobStatusResponse response = autoLabelingService.getJobStatus(jobUuid);
110
+ return ResponseEntity.ok(response);
111
+ } catch (IllegalArgumentException e) {
112
+ log.error("Invalid job ID format: {}", jobId);
113
+ return ResponseEntity.badRequest().build();
114
+ }
115
+ }
116
+
117
+ @PostMapping("/labeling/feedback")
118
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD', 'ROLE_USER')")
119
+ @Operation(summary = "Provide labeling feedback", description = "Submits feedback for improving labeling accuracy")
120
+ public ResponseEntity<LabelingFeedbackResponse> provideFeedback(
121
+ @Valid @RequestBody LabelingFeedbackRequest feedback) {
122
+ log.info("REST request to provide feedback for asset: {}", feedback.getAssetId());
123
+
124
+ try {
125
+ LabelingFeedbackResponse response = autoLabelingService.processFeedback(feedback);
126
+ return ResponseEntity.ok(response);
127
+ } catch (Exception e) {
128
+ log.error("Failed to process feedback: {}", e.getMessage(), e);
129
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
130
+ }
131
+ }
132
+
133
+ @PutMapping("/labeling/config/ml")
134
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD')")
135
+ @Operation(summary = "Update ML configuration", description = "Updates the machine learning configuration for auto-labeling")
136
+ public ResponseEntity<Map<String, Object>> updateMLConfiguration(
137
+ @Valid @RequestBody MLConfigRequest mlConfig) {
138
+ log.info("REST request to update ML configuration for provider: {}", mlConfig.getProviderType());
139
+
140
+ try {
141
+ boolean success = autoLabelingService.updateMLConfiguration(mlConfig);
142
+
143
+ if (success) {
144
+ return ResponseEntity.ok(Map.of(
145
+ "status", "SUCCESS",
146
+ "message", "ML configuration updated successfully",
147
+ "provider", mlConfig.getProviderType()
148
+ ));
149
+ } else {
150
+ return ResponseEntity.badRequest().body(Map.of(
151
+ "status", "ERROR",
152
+ "message", "ML configuration validation failed",
153
+ "provider", mlConfig.getProviderType()
154
+ ));
155
+ }
156
+ } catch (Exception e) {
157
+ log.error("Failed to update ML configuration: {}", e.getMessage(), e);
158
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of(
159
+ "status", "ERROR",
160
+ "message", "Failed to update ML configuration: " + e.getMessage()
161
+ ));
162
+ }
163
+ }
164
+
165
+ @PostMapping("/labeling/config/ml/test")
166
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD')")
167
+ @Operation(summary = "Test ML configuration", description = "Tests the connectivity and validity of an ML configuration")
168
+ public ResponseEntity<Map<String, Object>> testMLConfiguration(
169
+ @Valid @RequestBody MLConfigRequest mlConfig) {
170
+ log.info("REST request to test ML configuration for provider: {}", mlConfig.getProviderType());
171
+
172
+ try {
173
+ boolean connectionOk = llmClientFactory.testConnection(mlConfig);
174
+ boolean configValid = llmClientFactory.validateConfiguration(mlConfig);
175
+
176
+ Map<String, Object> result = Map.of(
177
+ "provider", mlConfig.getProviderType(),
178
+ "connectionTest", connectionOk ? "PASSED" : "FAILED",
179
+ "configurationTest", configValid ? "VALID" : "INVALID",
180
+ "overallStatus", (connectionOk && configValid) ? "SUCCESS" : "FAILED"
181
+ );
182
+
183
+ return ResponseEntity.ok(result);
184
+
185
+ } catch (Exception e) {
186
+ log.error("Failed to test ML configuration: {}", e.getMessage(), e);
187
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of(
188
+ "status", "ERROR",
189
+ "message", "Failed to test ML configuration: " + e.getMessage()
190
+ ));
191
+ }
192
+ }
193
+
194
+ @GetMapping("/providers")
195
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_DATA_STEWARD', 'ROLE_USER')")
196
+ @Operation(summary = "Get available LLM providers", description = "Retrieves information about all supported LLM providers")
197
+ public ResponseEntity<Map<String, Object>> getProviders() {
198
+ log.debug("REST request to get available LLM providers");
199
+
200
+ try {
201
+ Map<String, Map<String, Object>> capabilities = autoLabelingService.getProviderCapabilities();
202
+
203
+ Map<String, Object> response = Map.of(
204
+ "supportedProviders", llmClientFactory.getSupportedProviders(),
205
+ "providerCapabilities", capabilities
206
+ );
207
+
208
+ return ResponseEntity.ok(response);
209
+
210
+ } catch (Exception e) {
211
+ log.error("Failed to get providers: {}", e.getMessage(), e);
212
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
213
+ }
214
+ }
215
+
216
+ @GetMapping("/health")
217
+ @Operation(summary = "Service health check", description = "Checks the health status of the auto-labeling service")
218
+ public ResponseEntity<Map<String, Object>> healthCheck() {
219
+ log.debug("Health check requested");
220
+
221
+ try {
222
+ Map<String, Object> health = Map.of(
223
+ "status", "UP",
224
+ "service", "da-autolabel",
225
+ "timestamp", java.time.LocalDateTime.now(),
226
+ "availableProviders", llmClientFactory.getSupportedProviders().size()
227
+ );
228
+
229
+ return ResponseEntity.ok(health);
230
+
231
+ } catch (Exception e) {
232
+ log.error("Health check failed: {}", e.getMessage(), e);
233
+ return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(Map.of(
234
+ "status", "DOWN",
235
+ "error", e.getMessage()
236
+ ));
237
+ }
238
+ }
239
+ }
src/main/java/com/dalab/autolabel/controller/LabelingConfigController.java ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.controller;
2
+
3
+ import org.springframework.http.HttpStatus;
4
+ import org.springframework.http.ResponseEntity;
5
+ import org.springframework.security.access.prepost.PreAuthorize;
6
+ import org.springframework.web.bind.annotation.GetMapping;
7
+ import org.springframework.web.bind.annotation.PutMapping;
8
+ import org.springframework.web.bind.annotation.RequestBody;
9
+ import org.springframework.web.bind.annotation.RequestMapping;
10
+ import org.springframework.web.bind.annotation.RestController;
11
+
12
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
13
+ import com.dalab.autolabel.service.IMLConfigService;
14
+
15
+ import lombok.RequiredArgsConstructor;
16
+ import lombok.extern.slf4j.Slf4j;
17
+
18
+ /**
19
+ * REST controller for managing AutoLabel ML configurations.
20
+ */
21
+ @RestController
22
+ @RequestMapping("/api/v1/labeling")
23
+ @RequiredArgsConstructor
24
+ @Slf4j
25
+ public class LabelingConfigController {
26
+
27
+ private final IMLConfigService mlConfigService;
28
+
29
+ /**
30
+ * Updates the Machine Learning (ML) configuration for auto-labeling.
31
+ * Requires ADMIN role.
32
+ *
33
+ * @param mlConfigRequest The ML configuration request.
34
+ * @return ResponseEntity indicating success or failure.
35
+ */
36
+ @PutMapping("/config/ml")
37
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
38
+ public ResponseEntity<Void> updateMlConfiguration(@RequestBody MLConfigRequest mlConfigRequest) {
39
+ log.info("Received request to update ML configuration: {}", mlConfigRequest);
40
+ try {
41
+ mlConfigService.updateMlConfig(mlConfigRequest);
42
+ return ResponseEntity.ok().build();
43
+ } catch (IllegalArgumentException e) {
44
+ log.error("Invalid ML configuration provided: {}", e.getMessage());
45
+ return ResponseEntity.badRequest().build(); // Or a more specific error response
46
+ } catch (Exception e) {
47
+ log.error("Error updating ML configuration: {}", e.getMessage(), e);
48
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Retrieves the current Machine Learning (ML) configuration for auto-labeling.
54
+ * Requires ADMIN role.
55
+ *
56
+ * @return ResponseEntity with the current MLConfigRequest or not found/error.
57
+ */
58
+ @GetMapping("/config/ml")
59
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
60
+ public ResponseEntity<MLConfigRequest> getMlConfiguration() {
61
+ log.info("Received request to get ML configuration.");
62
+ try {
63
+ MLConfigRequest config = mlConfigService.getMlConfig();
64
+ if (config != null) {
65
+ return ResponseEntity.ok(config);
66
+ } else {
67
+ return ResponseEntity.notFound().build();
68
+ }
69
+ } catch (Exception e) {
70
+ log.error("Error retrieving ML configuration: {}", e.getMessage(), e);
71
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
72
+ }
73
+ }
74
+ }
src/main/java/com/dalab/autolabel/controller/LabelingJobController.java ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.controller;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackRequest;
4
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackResponse;
5
+ import com.dalab.autolabel.client.rest.dto.LabelingJobListResponse;
6
+ import com.dalab.autolabel.client.rest.dto.LabelingJobRequest;
7
+ import com.dalab.autolabel.client.rest.dto.LabelingJobResponse;
8
+ import com.dalab.autolabel.client.rest.dto.LabelingJobStatusResponse;
9
+ import com.dalab.autolabel.exception.AutoLabelingException;
10
+ import com.dalab.autolabel.service.ILabelingJobService;
11
+ import jakarta.validation.Valid;
12
+ import lombok.RequiredArgsConstructor;
13
+ import lombok.extern.slf4j.Slf4j;
14
+ import org.springframework.data.domain.Pageable;
15
+ import org.springframework.data.web.PageableDefault;
16
+ import org.springframework.http.HttpStatus;
17
+ import org.springframework.http.ResponseEntity;
18
+ import org.springframework.security.access.prepost.PreAuthorize;
19
+ import org.springframework.web.bind.annotation.*;
20
+
21
+ @RestController
22
+ @RequestMapping("/api/v1/labeling") // Changed base path to /api/v1/labeling
23
+ @RequiredArgsConstructor
24
+ @Slf4j
25
+ public class LabelingJobController {
26
+
27
+ private final ILabelingJobService labelingJobService;
28
+
29
+ /**
30
+ * Submits a new auto-labeling job.
31
+ * Requires DATA_STEWARD or ADMIN role.
32
+ *
33
+ * @param jobRequest The labeling job request.
34
+ * @return ResponseEntity with LabelingJobResponse or an error.
35
+ */
36
+ @PostMapping("/jobs")
37
+ @PreAuthorize("hasAnyAuthority('ROLE_DATA_STEWARD', 'ROLE_ADMIN')")
38
+ public ResponseEntity<LabelingJobResponse> submitLabelingJob(@Valid @RequestBody LabelingJobRequest jobRequest) {
39
+ log.info("Received request to submit labeling job: {}", jobRequest.getJobName());
40
+ try {
41
+ LabelingJobResponse jobResponse = labelingJobService.submitLabelingJob(jobRequest);
42
+ return ResponseEntity.status(HttpStatus.ACCEPTED).body(jobResponse);
43
+ } catch (AutoLabelingException e) {
44
+ log.error("Error submitting labeling job '{}': {}", jobRequest.getJobName(), e.getMessage());
45
+ return ResponseEntity.badRequest().body(LabelingJobResponse.builder().message(e.getMessage()).build());
46
+ } catch (Exception e) {
47
+ log.error("Unexpected error submitting labeling job '{}': {}", jobRequest.getJobName(), e.getMessage(), e);
48
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
49
+ .body(LabelingJobResponse.builder().message("An unexpected error occurred.").build());
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Retrieves the status of a specific auto-labeling job.
55
+ * Requires DATA_STEWARD, ADMIN, or USER role.
56
+ *
57
+ * @param jobId The ID of the job.
58
+ * @return ResponseEntity with LabelingJobStatusResponse or an error.
59
+ */
60
+ @GetMapping("/jobs/{jobId}")
61
+ @PreAuthorize("hasAnyAuthority('ROLE_DATA_STEWARD', 'ROLE_ADMIN', 'ROLE_USER')")
62
+ public ResponseEntity<LabelingJobStatusResponse> getJobStatus(@PathVariable String jobId) {
63
+ log.info("Received request to get status for labeling job ID: {}", jobId);
64
+ LabelingJobStatusResponse statusResponse = labelingJobService.getJobStatus(jobId);
65
+ if (statusResponse != null) {
66
+ return ResponseEntity.ok(statusResponse);
67
+ } else {
68
+ log.warn("Labeling job with ID '{}' not found.", jobId);
69
+ return ResponseEntity.notFound().build();
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Lists all auto-labeling jobs with pagination.
75
+ * Requires DATA_STEWARD, ADMIN, or USER role.
76
+ *
77
+ * @param pageable Pagination information.
78
+ * @return ResponseEntity with LabelingJobListResponse or an error.
79
+ */
80
+ @GetMapping("/jobs")
81
+ @PreAuthorize("hasAnyAuthority('ROLE_DATA_STEWARD', 'ROLE_ADMIN', 'ROLE_USER')")
82
+ public ResponseEntity<LabelingJobListResponse> listJobs(@PageableDefault(size = 20) Pageable pageable) {
83
+ log.info("Received request to list labeling jobs. Pagination: {}", pageable);
84
+ try {
85
+ LabelingJobListResponse listResponse = labelingJobService.listJobs(pageable);
86
+ return ResponseEntity.ok(listResponse);
87
+ } catch (Exception e) {
88
+ log.error("Error listing labeling jobs: {}", e.getMessage(), e);
89
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Submits feedback for auto-suggested labels on an asset.
95
+ * Requires DATA_STEWARD or ADMIN role.
96
+ *
97
+ * @param feedbackRequest The labeling feedback request.
98
+ * @return ResponseEntity with LabelingFeedbackResponse or an error.
99
+ */
100
+ @PostMapping("/feedback")
101
+ @PreAuthorize("hasAnyAuthority('ROLE_DATA_STEWARD', 'ROLE_ADMIN')")
102
+ public ResponseEntity<LabelingFeedbackResponse> submitLabelingFeedback(@Valid @RequestBody LabelingFeedbackRequest feedbackRequest) {
103
+ log.info("Received labeling feedback for asset ID: {}", feedbackRequest.getAssetId());
104
+ try {
105
+ LabelingFeedbackResponse feedbackResponse = labelingJobService.processLabelingFeedback(feedbackRequest);
106
+ return ResponseEntity.ok(feedbackResponse);
107
+ } catch (AutoLabelingException e) {
108
+ log.error("Error processing labeling feedback for asset '{}': {}", feedbackRequest.getAssetId(), e.getMessage());
109
+ return ResponseEntity.badRequest().body(LabelingFeedbackResponse.builder().message(e.getMessage()).build());
110
+ } catch (Exception e) {
111
+ log.error("Unexpected error processing labeling feedback for asset '{}': {}", feedbackRequest.getAssetId(), e.getMessage(), e);
112
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
113
+ .body(LabelingFeedbackResponse.builder().message("An unexpected error occurred while processing feedback.").build());
114
+ }
115
+ }
116
+ }
src/main/java/com/dalab/autolabel/entity/LabelingFeedbackEntity.java ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.entity;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackRequest;
4
+ import jakarta.persistence.*;
5
+ import lombok.AllArgsConstructor;
6
+ import lombok.Builder;
7
+ import lombok.Data;
8
+ import lombok.NoArgsConstructor;
9
+ import org.hibernate.annotations.JdbcTypeCode;
10
+ import org.hibernate.type.SqlTypes;
11
+
12
+ import java.time.LocalDateTime;
13
+ import java.util.List;
14
+ import java.util.Map;
15
+
16
+ /**
17
+ * JPA Entity representing labeling feedback provided by a user.
18
+ */
19
+ @Entity
20
+ @Table(name = "dalab_autolabel_feedback")
21
+ @Data
22
+ @NoArgsConstructor
23
+ @AllArgsConstructor
24
+ @Builder
25
+ public class LabelingFeedbackEntity {
26
+
27
+ @Id
28
+ private String feedbackId;
29
+
30
+ @Column(nullable = false)
31
+ private String assetId;
32
+
33
+ @Column(nullable = false)
34
+ private String labelingJobId; // The job that generated the suggestions being corrected
35
+
36
+ @JdbcTypeCode(SqlTypes.JSON)
37
+ @Column(columnDefinition = "jsonb", nullable = false)
38
+ private List<LabelingFeedbackRequest.FeedbackItem> feedbackItems;
39
+
40
+ private String userId; // User who provided the feedback
41
+
42
+ @JdbcTypeCode(SqlTypes.JSON)
43
+ @Column(columnDefinition = "jsonb")
44
+ private Map<String, String> additionalContext; // e.g., UI version, session ID
45
+
46
+ @Column(nullable = false)
47
+ private LocalDateTime receivedAt;
48
+
49
+ private LocalDateTime processedAt; // When the feedback was fully processed (e.g., catalog updated)
50
+
51
+ private String processingStatus; // E.g., "RECEIVED", "PROCESSING", "PROCESSED", "ERROR"
52
+ }
src/main/java/com/dalab/autolabel/entity/LabelingJobEntity.java ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.entity;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.LabelingJobRequest;
4
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
5
+ import jakarta.persistence.*;
6
+ import lombok.AllArgsConstructor;
7
+ import lombok.Builder;
8
+ import lombok.Data;
9
+ import lombok.NoArgsConstructor;
10
+ import org.hibernate.annotations.JdbcTypeCode;
11
+ import org.hibernate.type.SqlTypes;
12
+
13
+ import java.time.LocalDateTime;
14
+ import java.util.List;
15
+ import java.util.Map;
16
+
17
+ /**
18
+ * JPA Entity representing an auto-labeling job.
19
+ */
20
+ @Entity
21
+ @Table(name = "dalab_autolabel_job")
22
+ @Data
23
+ @NoArgsConstructor
24
+ @AllArgsConstructor
25
+ @Builder
26
+ public class LabelingJobEntity {
27
+
28
+ @Id
29
+ private String jobId;
30
+
31
+ private String jobName;
32
+
33
+ @Enumerated(EnumType.STRING)
34
+ private JobStatus status;
35
+
36
+ private LocalDateTime submittedAt;
37
+ private LocalDateTime startedAt;
38
+ private LocalDateTime lastUpdatedAt;
39
+ private LocalDateTime completedAt;
40
+
41
+ @JdbcTypeCode(SqlTypes.JSON)
42
+ @Column(columnDefinition = "jsonb")
43
+ private LabelingJobRequest.LabelingScope scope; // Store the original scope
44
+
45
+ @JdbcTypeCode(SqlTypes.JSON)
46
+ @Column(columnDefinition = "jsonb")
47
+ private MLConfigRequest effectiveMlConfig; // Store the ML config used for this job
48
+
49
+ private Integer totalAssetsToProcess;
50
+ private Integer assetsProcessed;
51
+ private Integer assetsSuccessfullyLabeled;
52
+ private Integer assetsFailed;
53
+
54
+ private String currentStageDescription;
55
+ private Double progressPercentage;
56
+
57
+ @ElementCollection(fetch = FetchType.EAGER)
58
+ @CollectionTable(name = "dalab_autolabel_job_errors", joinColumns = @JoinColumn(name = "job_id"))
59
+ @Column(name = "error_message", length = 2048) // Increased length for error messages
60
+ private List<String> errorMessages;
61
+
62
+ @JdbcTypeCode(SqlTypes.JSON)
63
+ @Column(columnDefinition = "jsonb")
64
+ private Map<String, Integer> labelSummary; // Key: Label Name, Value: Count
65
+
66
+ private String triggeredByUserId; // User who triggered the job
67
+
68
+ // Enum for JobStatus (can be shared or specific to this entity's package)
69
+ public enum JobStatus {
70
+ SUBMITTED, PREPARING_DATA, CALLING_LLM, APPLYING_LABELS, COMPLETED, FAILED, PARTIALLY_COMPLETED, CANCELLED
71
+ }
72
+ }
src/main/java/com/dalab/autolabel/exception/AutoLabelingException.java ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.exception;
2
+
3
+ /**
4
+ * Custom exception for the AutoLabel service.
5
+ */
6
+ public class AutoLabelingException extends RuntimeException {
7
+
8
+ private static final long serialVersionUID = 1L;
9
+
10
+ public AutoLabelingException(String message) {
11
+ super(message);
12
+ }
13
+
14
+ public AutoLabelingException(String message, Throwable cause) {
15
+ super(message, cause);
16
+ }
17
+ }
src/main/java/com/dalab/autolabel/llm/client/ILLMClient.java ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.client;
2
+
3
+ import java.util.List;
4
+ import java.util.Map;
5
+
6
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
7
+ import com.dalab.autolabel.llm.model.LLMResponse;
8
+ import com.dalab.autolabel.llm.model.LabelSuggestion;
9
+
10
+ /**
11
+ * Interface for a client that interacts with a Large Language Model (LLM).
12
+ * Provides abstraction for different LLM providers (OpenAI, Gemini, Ollama, etc.).
13
+ */
14
+ public interface ILLMClient {
15
+
16
+ /**
17
+ * Gets the provider name for this LLM client (e.g., "OPENAI", "GEMINI", "OLLAMA").
18
+ *
19
+ * @return The provider name.
20
+ */
21
+ String getProviderName();
22
+
23
+ /**
24
+ * Suggests a list of labels for the given asset content/metadata using the LLM.
25
+ *
26
+ * @param assetContent A string representation of the asset content or relevant metadata.
27
+ * @param mlConfig The ML configuration to use, containing model details, prompt templates, etc.
28
+ * @return A list of suggested label strings.
29
+ * @throws Exception if there is an error during LLM interaction.
30
+ */
31
+ List<String> suggestLabels(String assetContent, MLConfigRequest mlConfig) throws Exception;
32
+
33
+ /**
34
+ * Suggests labels with confidence scores for better decision making.
35
+ *
36
+ * @param assetContent A string representation of the asset content or relevant metadata.
37
+ * @param mlConfig The ML configuration to use, containing model details, prompt templates, etc.
38
+ * @return A list of label suggestions with confidence scores.
39
+ * @throws Exception if there is an error during LLM interaction.
40
+ */
41
+ List<LabelSuggestion> suggestLabelsWithConfidence(String assetContent, MLConfigRequest mlConfig) throws Exception;
42
+
43
+ /**
44
+ * Processes multiple assets in batch for efficiency.
45
+ *
46
+ * @param assetContents List of asset content strings to process.
47
+ * @param mlConfig The ML configuration to use.
48
+ * @return List of LLM responses corresponding to each input.
49
+ * @throws Exception if there is an error during LLM interaction.
50
+ */
51
+ List<LLMResponse> batchSuggestLabels(List<String> assetContents, MLConfigRequest mlConfig) throws Exception;
52
+
53
+ /**
54
+ * Validates that the ML configuration is compatible with this LLM client.
55
+ *
56
+ * @param mlConfig The ML configuration to validate.
57
+ * @return true if the configuration is valid for this client, false otherwise.
58
+ */
59
+ boolean validateConfiguration(MLConfigRequest mlConfig);
60
+
61
+ /**
62
+ * Gets the capabilities of this LLM client.
63
+ *
64
+ * @return A map of capabilities (e.g., "maxTokens", "supportsBatch", "supportsConfidence").
65
+ */
66
+ Map<String, Object> getCapabilities();
67
+
68
+ /**
69
+ * Tests the connection to the LLM provider with the given configuration.
70
+ *
71
+ * @param mlConfig The ML configuration to test.
72
+ * @return true if the connection is successful, false otherwise.
73
+ */
74
+ boolean testConnection(MLConfigRequest mlConfig);
75
+ }
src/main/java/com/dalab/autolabel/llm/client/LLMClientFactory.java ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.client;
2
+
3
+ import java.util.Collection;
4
+ import java.util.HashMap;
5
+ import java.util.List;
6
+ import java.util.Map;
7
+ import java.util.Set;
8
+
9
+ import org.slf4j.Logger;
10
+ import org.slf4j.LoggerFactory;
11
+ import org.springframework.beans.factory.annotation.Autowired;
12
+ import org.springframework.stereotype.Service;
13
+
14
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
15
+
16
+ /**
17
+ * Factory for creating and managing LLM clients.
18
+ * Handles provider selection and client lifecycle.
19
+ */
20
+ @Service
21
+ public class LLMClientFactory {
22
+
23
+ private static final Logger log = LoggerFactory.getLogger(LLMClientFactory.class);
24
+
25
+ private final Map<String, ILLMClient> clients;
26
+
27
+ @Autowired
28
+ public LLMClientFactory(List<ILLMClient> llmClients) {
29
+ this.clients = new HashMap<>();
30
+
31
+ // Register all available clients
32
+ for (ILLMClient client : llmClients) {
33
+ clients.put(client.getProviderName().toUpperCase(), client);
34
+ log.info("Registered LLM client: {}", client.getProviderName());
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Gets the appropriate LLM client based on the ML configuration.
40
+ *
41
+ * @param mlConfig The ML configuration specifying the provider
42
+ * @return The LLM client for the specified provider
43
+ * @throws IllegalArgumentException if the provider is not supported
44
+ */
45
+ public ILLMClient getClient(MLConfigRequest mlConfig) {
46
+ if (mlConfig == null || mlConfig.getProviderType() == null) {
47
+ throw new IllegalArgumentException("ML configuration and provider type cannot be null");
48
+ }
49
+
50
+ String providerType = mlConfig.getProviderType().toUpperCase();
51
+ ILLMClient client = clients.get(providerType);
52
+
53
+ if (client == null) {
54
+ throw new IllegalArgumentException("Unsupported LLM provider: " + providerType);
55
+ }
56
+
57
+ return client;
58
+ }
59
+
60
+ /**
61
+ * Gets all available LLM clients.
62
+ *
63
+ * @return Collection of all registered LLM clients
64
+ */
65
+ public Collection<ILLMClient> getAllClients() {
66
+ return clients.values();
67
+ }
68
+
69
+ /**
70
+ * Gets the names of all supported providers.
71
+ *
72
+ * @return Set of provider names
73
+ */
74
+ public Set<String> getSupportedProviders() {
75
+ return clients.keySet();
76
+ }
77
+
78
+ /**
79
+ * Validates if a provider is supported.
80
+ *
81
+ * @param providerType The provider type to check
82
+ * @return true if supported, false otherwise
83
+ */
84
+ public boolean isProviderSupported(String providerType) {
85
+ if (providerType == null) {
86
+ return false;
87
+ }
88
+ return clients.containsKey(providerType.toUpperCase());
89
+ }
90
+
91
+ /**
92
+ * Gets capabilities for all providers.
93
+ *
94
+ * @return Map of provider names to their capabilities
95
+ */
96
+ public Map<String, Map<String, Object>> getAllProviderCapabilities() {
97
+ Map<String, Map<String, Object>> allCapabilities = new HashMap<>();
98
+
99
+ for (Map.Entry<String, ILLMClient> entry : clients.entrySet()) {
100
+ allCapabilities.put(entry.getKey(), entry.getValue().getCapabilities());
101
+ }
102
+
103
+ return allCapabilities;
104
+ }
105
+
106
+ /**
107
+ * Tests connection for a specific configuration.
108
+ *
109
+ * @param mlConfig The ML configuration to test
110
+ * @return true if the connection test passes, false otherwise
111
+ */
112
+ public boolean testConnection(MLConfigRequest mlConfig) {
113
+ try {
114
+ ILLMClient client = getClient(mlConfig);
115
+ return client.testConnection(mlConfig);
116
+ } catch (Exception e) {
117
+ log.error("Connection test failed: {}", e.getMessage(), e);
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Validates a configuration for a specific provider.
124
+ *
125
+ * @param mlConfig The ML configuration to validate
126
+ * @return true if the configuration is valid, false otherwise
127
+ */
128
+ public boolean validateConfiguration(MLConfigRequest mlConfig) {
129
+ try {
130
+ ILLMClient client = getClient(mlConfig);
131
+ return client.validateConfiguration(mlConfig);
132
+ } catch (Exception e) {
133
+ log.error("Configuration validation failed: {}", e.getMessage(), e);
134
+ return false;
135
+ }
136
+ }
137
+ }
src/main/java/com/dalab/autolabel/llm/client/impl/MockLLMClient.java ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.client.impl;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.ArrayList;
5
+ import java.util.Arrays;
6
+ import java.util.HashMap;
7
+ import java.util.List;
8
+ import java.util.Map;
9
+ import java.util.Random;
10
+
11
+ import org.springframework.stereotype.Component;
12
+
13
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
14
+ import com.dalab.autolabel.llm.client.ILLMClient;
15
+ import com.dalab.autolabel.llm.model.LLMResponse;
16
+ import com.dalab.autolabel.llm.model.LabelSuggestion;
17
+
18
+ import lombok.extern.slf4j.Slf4j;
19
+
20
+ @Component("mockLLMClient")
21
+ @Slf4j
22
+ public class MockLLMClient implements ILLMClient {
23
+
24
+ private static final List<String> MOCK_LABELS = Arrays.asList(
25
+ "PII", "Confidential", "Public", "Sensitive", "FinancialData",
26
+ "HealthRecord", "SourceCode", "UserGeneratedContent", "Archive", "Temporary"
27
+ );
28
+ private final Random random = new Random();
29
+
30
+ @Override
31
+ public String getProviderName() {
32
+ return "MOCK";
33
+ }
34
+
35
+ @Override
36
+ public List<String> suggestLabels(String assetContent, MLConfigRequest mlConfig) throws Exception {
37
+ log.info("MockLLMClient: Received request to suggest labels for asset content (first 50 chars): '{}' with config: {}",
38
+ assetContent.substring(0, Math.min(assetContent.length(), 50)), mlConfig.getModelName());
39
+
40
+ // Simulate network delay
41
+ Thread.sleep(50 + random.nextInt(200)); // Simulate 50-250ms delay
42
+
43
+ int numberOfLabels = 1 + random.nextInt(3); // Suggest 1 to 3 labels
44
+ List<String> suggested = new java.util.ArrayList<>();
45
+ for (int i = 0; i < numberOfLabels; i++) {
46
+ suggested.add(MOCK_LABELS.get(random.nextInt(MOCK_LABELS.size())));
47
+ }
48
+
49
+ log.info("MockLLMClient: Suggested labels: {}", suggested);
50
+ return suggested;
51
+ }
52
+
53
+ @Override
54
+ public boolean validateConfiguration(MLConfigRequest mlConfig) {
55
+ log.info("MockLLMClient: Validating configuration for model: {}", mlConfig.getModelName());
56
+ // Mock validation - always return true for testing
57
+ return true;
58
+ }
59
+
60
+ @Override
61
+ public boolean testConnection(MLConfigRequest mlConfig) {
62
+ log.info("MockLLMClient: Testing connection for model: {}", mlConfig.getModelName());
63
+ // Mock connection test - always return true for testing
64
+ return true;
65
+ }
66
+
67
+ @Override
68
+ public Map<String, Object> getCapabilities() {
69
+ Map<String, Object> capabilities = new HashMap<>();
70
+ capabilities.put("maxTokens", 4096);
71
+ capabilities.put("supportsBatch", true);
72
+ capabilities.put("supportsConfidence", true);
73
+ capabilities.put("supportsReasoning", true);
74
+ capabilities.put("provider", "MOCK");
75
+ capabilities.put("cost", 0.0);
76
+ return capabilities;
77
+ }
78
+
79
+ @Override
80
+ public List<LabelSuggestion> suggestLabelsWithConfidence(String assetContent, MLConfigRequest mlConfig) throws Exception {
81
+ log.info("MockLLMClient: Received request to suggest labels with confidence for asset content (first 50 chars): '{}' with config: {}",
82
+ assetContent.substring(0, Math.min(assetContent.length(), 50)), mlConfig.getModelName());
83
+
84
+ // Simulate processing delay
85
+ Thread.sleep(100 + random.nextInt(200));
86
+
87
+ // Generate mock suggestions with confidence scores
88
+ List<LabelSuggestion> suggestions = new ArrayList<>();
89
+ int numSuggestions = 2 + random.nextInt(4); // 2-5 suggestions
90
+
91
+ for (int i = 0; i < numSuggestions; i++) {
92
+ String label = MOCK_LABELS.get(random.nextInt(MOCK_LABELS.size()));
93
+ double confidence = 0.5 + (random.nextDouble() * 0.5); // 0.5-1.0 confidence
94
+
95
+ suggestions.add(LabelSuggestion.builder()
96
+ .label(label)
97
+ .confidence(confidence)
98
+ .reasoning("Mock reasoning for " + label + " based on content analysis")
99
+ .category("MOCK_CATEGORY")
100
+ .build());
101
+ }
102
+
103
+ return suggestions;
104
+ }
105
+
106
+ @Override
107
+ public List<LLMResponse> batchSuggestLabels(List<String> assetContents, MLConfigRequest mlConfig) throws Exception {
108
+ log.info("MockLLMClient: Processing batch of {} assets with config: {}", assetContents.size(), mlConfig.getModelName());
109
+
110
+ List<LLMResponse> responses = new ArrayList<>();
111
+
112
+ for (String content : assetContents) {
113
+ try {
114
+ List<LabelSuggestion> suggestions = suggestLabelsWithConfidence(content, mlConfig);
115
+
116
+ LLMResponse response = LLMResponse.builder()
117
+ .successful(true)
118
+ .suggestions(suggestions)
119
+ .processingTimeMs(100L + random.nextInt(200))
120
+ .timestamp(LocalDateTime.now())
121
+ .build();
122
+
123
+ responses.add(response);
124
+ } catch (Exception e) {
125
+ LLMResponse errorResponse = LLMResponse.builder()
126
+ .successful(false)
127
+ .errorMessage("Mock error: " + e.getMessage())
128
+ .processingTimeMs(50L)
129
+ .timestamp(LocalDateTime.now())
130
+ .build();
131
+
132
+ responses.add(errorResponse);
133
+ }
134
+ }
135
+
136
+ return responses;
137
+ }
138
+ }
src/main/java/com/dalab/autolabel/llm/client/impl/OllamaLLMClient.java ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.client.impl;
2
+
3
+ import java.io.IOException;
4
+ import java.nio.charset.StandardCharsets;
5
+ import java.time.LocalDateTime;
6
+ import java.util.ArrayList;
7
+ import java.util.Arrays;
8
+ import java.util.HashMap;
9
+ import java.util.List;
10
+ import java.util.Map;
11
+ import java.util.regex.Matcher;
12
+ import java.util.regex.Pattern;
13
+ import java.util.stream.Collectors;
14
+
15
+ import org.apache.hc.client5.http.classic.methods.HttpPost;
16
+ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
17
+ import org.apache.hc.client5.http.impl.classic.HttpClients;
18
+ import org.apache.hc.core5.http.io.entity.StringEntity;
19
+ import org.slf4j.Logger;
20
+ import org.slf4j.LoggerFactory;
21
+ import org.springframework.stereotype.Component;
22
+
23
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
24
+ import com.dalab.autolabel.llm.client.ILLMClient;
25
+ import com.dalab.autolabel.llm.model.LLMResponse;
26
+ import com.dalab.autolabel.llm.model.LabelSuggestion;
27
+ import com.fasterxml.jackson.core.JsonProcessingException;
28
+ import com.fasterxml.jackson.databind.JsonNode;
29
+ import com.fasterxml.jackson.databind.ObjectMapper;
30
+
31
+ /**
32
+ * Ollama implementation of the LLM client for local/self-hosted open-source models.
33
+ * Supports models like Llama2, CodeLlama, Mistral, Gemma, DeepSeek, etc.
34
+ */
35
+ @Component
36
+ public class OllamaLLMClient implements ILLMClient {
37
+
38
+ private static final Logger log = LoggerFactory.getLogger(OllamaLLMClient.class);
39
+ private static final String PROVIDER_NAME = "OLLAMA";
40
+ private static final ObjectMapper objectMapper = new ObjectMapper();
41
+
42
+ // Default Ollama endpoint
43
+ private static final String DEFAULT_BASE_URL = "http://localhost:11434";
44
+
45
+ // Default prompt template for label suggestion
46
+ private static final String DEFAULT_PROMPT_TEMPLATE = """
47
+ You are a data governance expert. Analyze the following data asset and suggest appropriate labels.
48
+
49
+ Data Asset Information:
50
+ {metadata_summary}
51
+
52
+ Based on this information, suggest up to {max_suggestions} relevant data classification labels. Focus on:
53
+ 1. Data sensitivity levels (PII, Confidential, Internal, Public)
54
+ 2. Business context and domain
55
+ 3. Technical characteristics
56
+ 4. Regulatory compliance needs
57
+
58
+ Provide your response as a JSON array with this format:
59
+ [
60
+ {
61
+ "label": "label name",
62
+ "confidence": 0.95,
63
+ "reasoning": "explanation for this label",
64
+ "category": "PII|BUSINESS|TECHNICAL|COMPLIANCE"
65
+ }
66
+ ]
67
+
68
+ Only return the JSON array, no additional text.
69
+ """;
70
+
71
+ @Override
72
+ public String getProviderName() {
73
+ return PROVIDER_NAME;
74
+ }
75
+
76
+ @Override
77
+ public List<String> suggestLabels(String assetContent, MLConfigRequest mlConfig) throws Exception {
78
+ List<LabelSuggestion> suggestions = suggestLabelsWithConfidence(assetContent, mlConfig);
79
+ return suggestions.stream()
80
+ .map(LabelSuggestion::getLabel)
81
+ .collect(Collectors.toList());
82
+ }
83
+
84
+ @Override
85
+ public List<LabelSuggestion> suggestLabelsWithConfidence(String assetContent, MLConfigRequest mlConfig) throws Exception {
86
+ log.info("Requesting label suggestions from Ollama for content: {}",
87
+ assetContent.length() > 100 ? assetContent.substring(0, 100) + "..." : assetContent);
88
+
89
+ try {
90
+ String baseUrl = mlConfig.getBaseUrl() != null ? mlConfig.getBaseUrl() : DEFAULT_BASE_URL;
91
+ String prompt = buildPrompt(assetContent, mlConfig);
92
+
93
+ Map<String, Object> requestBody = new HashMap<>();
94
+ requestBody.put("model", mlConfig.getModelName() != null ? mlConfig.getModelName() : "llama2");
95
+ requestBody.put("prompt", prompt);
96
+ requestBody.put("stream", false);
97
+
98
+ // Add optional parameters
99
+ Map<String, Object> options = new HashMap<>();
100
+ options.put("temperature", getDoubleParameter(mlConfig, "temperature", 0.7));
101
+ options.put("top_p", getDoubleParameter(mlConfig, "top_p", 0.9));
102
+ options.put("max_tokens", getIntegerParameter(mlConfig, "max_tokens", 1000));
103
+ requestBody.put("options", options);
104
+
105
+ String responseJson = makeHttpRequest(baseUrl + "/api/generate", requestBody);
106
+ return parseOllamaResponse(responseJson);
107
+
108
+ } catch (Exception e) {
109
+ log.error("Error getting label suggestions from Ollama: {}", e.getMessage(), e);
110
+ throw new Exception("Failed to get suggestions from Ollama: " + e.getMessage(), e);
111
+ }
112
+ }
113
+
114
+ @Override
115
+ public List<LLMResponse> batchSuggestLabels(List<String> assetContents, MLConfigRequest mlConfig) throws Exception {
116
+ log.info("Processing batch of {} assets for label suggestions with Ollama", assetContents.size());
117
+
118
+ List<LLMResponse> responses = new ArrayList<>();
119
+ long startTime = System.currentTimeMillis();
120
+
121
+ for (String content : assetContents) {
122
+ long itemStartTime = System.currentTimeMillis();
123
+ try {
124
+ List<LabelSuggestion> suggestions = suggestLabelsWithConfidence(content, mlConfig);
125
+ long processingTime = System.currentTimeMillis() - itemStartTime;
126
+
127
+ LLMResponse response = LLMResponse.builder()
128
+ .inputContent(content)
129
+ .suggestions(suggestions)
130
+ .processingTimeMs(processingTime)
131
+ .timestamp(LocalDateTime.now())
132
+ .provider(PROVIDER_NAME)
133
+ .modelName(mlConfig.getModelName())
134
+ .successful(true)
135
+ .overallConfidence(calculateOverallConfidence(suggestions))
136
+ .estimatedCost(0.0) // Local models have no API cost
137
+ .build();
138
+
139
+ responses.add(response);
140
+
141
+ } catch (Exception e) {
142
+ log.error("Error processing asset in batch: {}", e.getMessage(), e);
143
+
144
+ LLMResponse errorResponse = LLMResponse.builder()
145
+ .inputContent(content)
146
+ .suggestions(new ArrayList<>())
147
+ .processingTimeMs(System.currentTimeMillis() - itemStartTime)
148
+ .timestamp(LocalDateTime.now())
149
+ .provider(PROVIDER_NAME)
150
+ .modelName(mlConfig.getModelName())
151
+ .successful(false)
152
+ .errorMessage(e.getMessage())
153
+ .estimatedCost(0.0)
154
+ .build();
155
+
156
+ responses.add(errorResponse);
157
+ }
158
+ }
159
+
160
+ log.info("Completed Ollama batch processing in {} ms", System.currentTimeMillis() - startTime);
161
+ return responses;
162
+ }
163
+
164
+ @Override
165
+ public boolean validateConfiguration(MLConfigRequest mlConfig) {
166
+ if (mlConfig == null) {
167
+ return false;
168
+ }
169
+
170
+ // Check provider type
171
+ if (!PROVIDER_NAME.equalsIgnoreCase(mlConfig.getProviderType())) {
172
+ return false;
173
+ }
174
+
175
+ // For Ollama, API key is not required but model name is important
176
+ String modelName = mlConfig.getModelName();
177
+ if (modelName == null || modelName.trim().isEmpty()) {
178
+ log.warn("Model name not specified for Ollama client");
179
+ return false;
180
+ }
181
+
182
+ return true;
183
+ }
184
+
185
+ @Override
186
+ public Map<String, Object> getCapabilities() {
187
+ Map<String, Object> capabilities = new HashMap<>();
188
+ capabilities.put("maxTokens", 4096); // Depends on the model
189
+ capabilities.put("supportsBatch", true);
190
+ capabilities.put("supportsConfidence", true);
191
+ capabilities.put("supportsReasoning", true);
192
+ capabilities.put("supportedModels", Arrays.asList(
193
+ "llama2", "llama2:7b", "llama2:13b", "llama2:70b",
194
+ "codellama", "codellama:7b", "codellama:13b", "codellama:34b",
195
+ "mistral", "mistral:7b", "mistral:instruct",
196
+ "gemma:2b", "gemma:7b",
197
+ "deepseek-coder", "deepseek-coder:6.7b", "deepseek-coder:33b"
198
+ ));
199
+ capabilities.put("costPerToken", 0.0); // Local models have no API cost
200
+ capabilities.put("requiresLocalSetup", true);
201
+ return capabilities;
202
+ }
203
+
204
+ @Override
205
+ public boolean testConnection(MLConfigRequest mlConfig) {
206
+ try {
207
+ if (!validateConfiguration(mlConfig)) {
208
+ return false;
209
+ }
210
+
211
+ String baseUrl = mlConfig.getBaseUrl() != null ? mlConfig.getBaseUrl() : DEFAULT_BASE_URL;
212
+
213
+ // Test with a simple request
214
+ Map<String, Object> testRequest = new HashMap<>();
215
+ testRequest.put("model", mlConfig.getModelName());
216
+ testRequest.put("prompt", "Test connection. Respond with OK.");
217
+ testRequest.put("stream", false);
218
+
219
+ Map<String, Object> options = new HashMap<>();
220
+ options.put("max_tokens", 10);
221
+ testRequest.put("options", options);
222
+
223
+ String response = makeHttpRequest(baseUrl + "/api/generate", testRequest);
224
+ return response != null && !response.trim().isEmpty();
225
+
226
+ } catch (Exception e) {
227
+ log.error("Connection test failed for Ollama: {}", e.getMessage(), e);
228
+ return false;
229
+ }
230
+ }
231
+
232
+ private String makeHttpRequest(String url, Map<String, Object> requestBody) throws Exception {
233
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
234
+ HttpPost httpPost = new HttpPost(url);
235
+ httpPost.setHeader("Content-Type", "application/json");
236
+
237
+ String jsonBody = objectMapper.writeValueAsString(requestBody);
238
+ httpPost.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
239
+
240
+ return httpClient.execute(httpPost, response -> {
241
+ if (response.getCode() == 200) {
242
+ return new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8);
243
+ } else {
244
+ throw new IOException("HTTP " + response.getCode() + ": " + response.getReasonPhrase());
245
+ }
246
+ });
247
+
248
+ } catch (Exception e) {
249
+ log.error("HTTP request failed: {}", e.getMessage(), e);
250
+ throw e;
251
+ }
252
+ }
253
+
254
+ private String buildPrompt(String assetContent, MLConfigRequest mlConfig) {
255
+ String template = mlConfig.getPromptTemplate() != null ?
256
+ mlConfig.getPromptTemplate() : DEFAULT_PROMPT_TEMPLATE;
257
+
258
+ Integer maxSuggestions = mlConfig.getSuggestionConfig() != null ?
259
+ mlConfig.getSuggestionConfig().getMaxSuggestions() : 5;
260
+
261
+ return template
262
+ .replace("{metadata_summary}", assetContent)
263
+ .replace("{max_suggestions}", String.valueOf(maxSuggestions));
264
+ }
265
+
266
+ private List<LabelSuggestion> parseOllamaResponse(String responseJson) {
267
+ try {
268
+ JsonNode responseNode = objectMapper.readTree(responseJson);
269
+ String responseText = responseNode.path("response").asText();
270
+
271
+ return parseLabelsFromResponse(responseText);
272
+
273
+ } catch (JsonProcessingException e) {
274
+ log.error("Failed to parse Ollama response JSON: {}", e.getMessage(), e);
275
+ return new ArrayList<>();
276
+ }
277
+ }
278
+
279
+ private List<LabelSuggestion> parseLabelsFromResponse(String response) {
280
+ List<LabelSuggestion> suggestions = new ArrayList<>();
281
+
282
+ try {
283
+ // Try to parse as JSON first
284
+ JsonNode jsonArray = objectMapper.readTree(response);
285
+ if (jsonArray.isArray()) {
286
+ for (JsonNode item : jsonArray) {
287
+ LabelSuggestion suggestion = LabelSuggestion.builder()
288
+ .label(item.path("label").asText())
289
+ .confidence(item.path("confidence").asDouble(0.5))
290
+ .reasoning(item.path("reasoning").asText())
291
+ .category(item.path("category").asText())
292
+ .build();
293
+ suggestions.add(suggestion);
294
+ }
295
+ return suggestions;
296
+ }
297
+ } catch (JsonProcessingException e) {
298
+ log.debug("Response is not valid JSON, trying text parsing: {}", e.getMessage());
299
+ }
300
+
301
+ // Fallback: parse as plain text
302
+ return parseLabelsFromText(response);
303
+ }
304
+
305
+ private List<LabelSuggestion> parseLabelsFromText(String response) {
306
+ List<LabelSuggestion> suggestions = new ArrayList<>();
307
+
308
+ // Look for patterns like "- Label" or "1. Label" or just lines with labels
309
+ Pattern labelPattern = Pattern.compile("(?:[-*]|\\d+\\.)\\s*([A-Za-z_][A-Za-z0-9_\\s]*)", Pattern.MULTILINE);
310
+ Matcher matcher = labelPattern.matcher(response);
311
+
312
+ while (matcher.find()) {
313
+ String label = matcher.group(1).trim();
314
+ suggestions.add(LabelSuggestion.builder()
315
+ .label(label)
316
+ .confidence(0.6) // Default confidence for text parsing from local models
317
+ .reasoning("Extracted from Ollama text response")
318
+ .category("UNKNOWN")
319
+ .build());
320
+ }
321
+
322
+ return suggestions;
323
+ }
324
+
325
+ private double calculateOverallConfidence(List<LabelSuggestion> suggestions) {
326
+ if (suggestions.isEmpty()) {
327
+ return 0.0;
328
+ }
329
+
330
+ return suggestions.stream()
331
+ .mapToDouble(s -> s.getConfidence() != null ? s.getConfidence() : 0.5)
332
+ .average()
333
+ .orElse(0.0);
334
+ }
335
+
336
+ private Double getDoubleParameter(MLConfigRequest mlConfig, String key, Double defaultValue) {
337
+ if (mlConfig.getAdditionalParameters() != null && mlConfig.getAdditionalParameters().containsKey(key)) {
338
+ Object value = mlConfig.getAdditionalParameters().get(key);
339
+ if (value instanceof Number) {
340
+ return ((Number) value).doubleValue();
341
+ }
342
+ }
343
+ return defaultValue;
344
+ }
345
+
346
+ private Integer getIntegerParameter(MLConfigRequest mlConfig, String key, Integer defaultValue) {
347
+ if (mlConfig.getAdditionalParameters() != null && mlConfig.getAdditionalParameters().containsKey(key)) {
348
+ Object value = mlConfig.getAdditionalParameters().get(key);
349
+ if (value instanceof Number) {
350
+ return ((Number) value).intValue();
351
+ }
352
+ }
353
+ return defaultValue;
354
+ }
355
+ }
src/main/java/com/dalab/autolabel/llm/client/impl/OpenAiLLMClient.java ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.client.impl;
2
+
3
+ import java.time.Duration;
4
+ import java.time.LocalDateTime;
5
+ import java.util.ArrayList;
6
+ import java.util.Arrays;
7
+ import java.util.HashMap;
8
+ import java.util.List;
9
+ import java.util.Map;
10
+ import java.util.Set;
11
+ import java.util.regex.Matcher;
12
+ import java.util.regex.Pattern;
13
+ import java.util.stream.Collectors;
14
+
15
+ import org.slf4j.Logger;
16
+ import org.slf4j.LoggerFactory;
17
+ import org.springframework.stereotype.Component;
18
+
19
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
20
+ import com.dalab.autolabel.llm.client.ILLMClient;
21
+ import com.dalab.autolabel.llm.model.LLMResponse;
22
+ import com.dalab.autolabel.llm.model.LabelSuggestion;
23
+ import com.fasterxml.jackson.core.JsonProcessingException;
24
+ import com.fasterxml.jackson.databind.JsonNode;
25
+ import com.fasterxml.jackson.databind.ObjectMapper;
26
+ import com.theokanning.openai.completion.chat.ChatCompletionRequest;
27
+ import com.theokanning.openai.completion.chat.ChatCompletionResult;
28
+ import com.theokanning.openai.completion.chat.ChatMessage;
29
+ import com.theokanning.openai.completion.chat.ChatMessageRole;
30
+ import com.theokanning.openai.service.OpenAiService;
31
+
32
+ /**
33
+ * OpenAI GPT implementation of the LLM client.
34
+ * Supports GPT-3.5-turbo, GPT-4, and other OpenAI models.
35
+ */
36
+ @Component
37
+ public class OpenAiLLMClient implements ILLMClient {
38
+
39
+ private static final Logger log = LoggerFactory.getLogger(OpenAiLLMClient.class);
40
+ private static final String PROVIDER_NAME = "OPENAI";
41
+ private static final ObjectMapper objectMapper = new ObjectMapper();
42
+
43
+ // Default prompt template for label suggestion
44
+ private static final String DEFAULT_PROMPT_TEMPLATE = """
45
+ You are a data governance AI assistant. Based on the following asset metadata, suggest relevant data labels.
46
+
47
+ Asset Information:
48
+ {metadata_summary}
49
+
50
+ Please suggest up to {max_suggestions} relevant labels for this data asset. Focus on:
51
+ - Data sensitivity (PII, Confidential, Public, etc.)
52
+ - Business purpose and domain
53
+ - Technical characteristics
54
+ - Compliance requirements
55
+
56
+ Respond with a JSON array of objects, each containing:
57
+ - "label": the suggested label text
58
+ - "confidence": confidence score (0.0 to 1.0)
59
+ - "reasoning": brief explanation for the suggestion
60
+ - "category": category type (PII, BUSINESS, TECHNICAL, COMPLIANCE)
61
+
62
+ Example response:
63
+ [
64
+ {"label": "PII", "confidence": 0.95, "reasoning": "Contains personal identifiers", "category": "PII"},
65
+ {"label": "Customer Data", "confidence": 0.87, "reasoning": "Related to customer information", "category": "BUSINESS"}
66
+ ]
67
+ """;
68
+
69
+ @Override
70
+ public String getProviderName() {
71
+ return PROVIDER_NAME;
72
+ }
73
+
74
+ @Override
75
+ public List<String> suggestLabels(String assetContent, MLConfigRequest mlConfig) throws Exception {
76
+ List<LabelSuggestion> suggestions = suggestLabelsWithConfidence(assetContent, mlConfig);
77
+ return suggestions.stream()
78
+ .map(LabelSuggestion::getLabel)
79
+ .collect(Collectors.toList());
80
+ }
81
+
82
+ @Override
83
+ public List<LabelSuggestion> suggestLabelsWithConfidence(String assetContent, MLConfigRequest mlConfig) throws Exception {
84
+ log.info("Requesting label suggestions from OpenAI for content: {}",
85
+ assetContent.length() > 100 ? assetContent.substring(0, 100) + "..." : assetContent);
86
+
87
+ try {
88
+ OpenAiService service = createOpenAiService(mlConfig);
89
+ String prompt = buildPrompt(assetContent, mlConfig);
90
+
91
+ ChatCompletionRequest request = ChatCompletionRequest.builder()
92
+ .model(mlConfig.getModelName() != null ? mlConfig.getModelName() : "gpt-3.5-turbo")
93
+ .messages(List.of(new ChatMessage(ChatMessageRole.USER.value(), prompt)))
94
+ .temperature(getDoubleParameter(mlConfig, "temperature", 0.7))
95
+ .maxTokens(getIntegerParameter(mlConfig, "maxTokens", 1000))
96
+ .build();
97
+
98
+ ChatCompletionResult result = service.createChatCompletion(request);
99
+
100
+ if (result.getChoices() != null && !result.getChoices().isEmpty()) {
101
+ String responseContent = result.getChoices().get(0).getMessage().getContent();
102
+ return parseLabelsFromResponse(responseContent);
103
+ }
104
+
105
+ log.warn("No choices returned from OpenAI response");
106
+ return new ArrayList<>();
107
+
108
+ } catch (Exception e) {
109
+ log.error("Error getting label suggestions from OpenAI: {}", e.getMessage(), e);
110
+ throw new Exception("Failed to get suggestions from OpenAI: " + e.getMessage(), e);
111
+ }
112
+ }
113
+
114
+ @Override
115
+ public List<LLMResponse> batchSuggestLabels(List<String> assetContents, MLConfigRequest mlConfig) throws Exception {
116
+ log.info("Processing batch of {} assets for label suggestions", assetContents.size());
117
+
118
+ List<LLMResponse> responses = new ArrayList<>();
119
+ long startTime = System.currentTimeMillis();
120
+
121
+ for (String content : assetContents) {
122
+ long itemStartTime = System.currentTimeMillis();
123
+ try {
124
+ List<LabelSuggestion> suggestions = suggestLabelsWithConfidence(content, mlConfig);
125
+ long processingTime = System.currentTimeMillis() - itemStartTime;
126
+
127
+ LLMResponse response = LLMResponse.builder()
128
+ .inputContent(content)
129
+ .suggestions(suggestions)
130
+ .processingTimeMs(processingTime)
131
+ .timestamp(LocalDateTime.now())
132
+ .provider(PROVIDER_NAME)
133
+ .modelName(mlConfig.getModelName())
134
+ .successful(true)
135
+ .overallConfidence(calculateOverallConfidence(suggestions))
136
+ .build();
137
+
138
+ responses.add(response);
139
+
140
+ } catch (Exception e) {
141
+ log.error("Error processing asset in batch: {}", e.getMessage(), e);
142
+
143
+ LLMResponse errorResponse = LLMResponse.builder()
144
+ .inputContent(content)
145
+ .suggestions(new ArrayList<>())
146
+ .processingTimeMs(System.currentTimeMillis() - itemStartTime)
147
+ .timestamp(LocalDateTime.now())
148
+ .provider(PROVIDER_NAME)
149
+ .modelName(mlConfig.getModelName())
150
+ .successful(false)
151
+ .errorMessage(e.getMessage())
152
+ .build();
153
+
154
+ responses.add(errorResponse);
155
+ }
156
+ }
157
+
158
+ log.info("Completed batch processing in {} ms", System.currentTimeMillis() - startTime);
159
+ return responses;
160
+ }
161
+
162
+ @Override
163
+ public boolean validateConfiguration(MLConfigRequest mlConfig) {
164
+ if (mlConfig == null) {
165
+ return false;
166
+ }
167
+
168
+ // Check provider type
169
+ if (!PROVIDER_NAME.equalsIgnoreCase(mlConfig.getProviderType())) {
170
+ return false;
171
+ }
172
+
173
+ // Check required fields
174
+ if (mlConfig.getApiKey() == null || mlConfig.getApiKey().trim().isEmpty()) {
175
+ return false;
176
+ }
177
+
178
+ // Validate model name if provided
179
+ String modelName = mlConfig.getModelName();
180
+ if (modelName != null && !isValidOpenAiModel(modelName)) {
181
+ log.warn("Unknown OpenAI model: {}", modelName);
182
+ // Still return true as new models might be added
183
+ }
184
+
185
+ return true;
186
+ }
187
+
188
+ @Override
189
+ public Map<String, Object> getCapabilities() {
190
+ Map<String, Object> capabilities = new HashMap<>();
191
+ capabilities.put("maxTokens", 4096);
192
+ capabilities.put("supportsBatch", true);
193
+ capabilities.put("supportsConfidence", true);
194
+ capabilities.put("supportsReasoning", true);
195
+ capabilities.put("supportedModels", Arrays.asList(
196
+ "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "gpt-4-turbo", "gpt-4o"
197
+ ));
198
+ capabilities.put("costPerToken", 0.0000015); // Approximate for GPT-3.5-turbo
199
+ return capabilities;
200
+ }
201
+
202
+ @Override
203
+ public boolean testConnection(MLConfigRequest mlConfig) {
204
+ try {
205
+ if (!validateConfiguration(mlConfig)) {
206
+ return false;
207
+ }
208
+
209
+ OpenAiService service = createOpenAiService(mlConfig);
210
+
211
+ // Simple test request
212
+ ChatCompletionRequest testRequest = ChatCompletionRequest.builder()
213
+ .model(mlConfig.getModelName() != null ? mlConfig.getModelName() : "gpt-3.5-turbo")
214
+ .messages(List.of(new ChatMessage(ChatMessageRole.USER.value(), "Test connection. Respond with 'OK'.")))
215
+ .maxTokens(10)
216
+ .build();
217
+
218
+ ChatCompletionResult result = service.createChatCompletion(testRequest);
219
+ return result.getChoices() != null && !result.getChoices().isEmpty();
220
+
221
+ } catch (Exception e) {
222
+ log.error("Connection test failed for OpenAI: {}", e.getMessage(), e);
223
+ return false;
224
+ }
225
+ }
226
+
227
+ private OpenAiService createOpenAiService(MLConfigRequest mlConfig) {
228
+ Duration timeout = Duration.ofSeconds(getIntegerParameter(mlConfig, "timeoutSeconds", 60));
229
+ return new OpenAiService(mlConfig.getApiKey(), timeout);
230
+ }
231
+
232
+ private String buildPrompt(String assetContent, MLConfigRequest mlConfig) {
233
+ String template = mlConfig.getPromptTemplate() != null ?
234
+ mlConfig.getPromptTemplate() : DEFAULT_PROMPT_TEMPLATE;
235
+
236
+ Integer maxSuggestions = mlConfig.getSuggestionConfig() != null ?
237
+ mlConfig.getSuggestionConfig().getMaxSuggestions() : 5;
238
+
239
+ return template
240
+ .replace("{metadata_summary}", assetContent)
241
+ .replace("{max_suggestions}", String.valueOf(maxSuggestions));
242
+ }
243
+
244
+ private List<LabelSuggestion> parseLabelsFromResponse(String response) {
245
+ List<LabelSuggestion> suggestions = new ArrayList<>();
246
+
247
+ try {
248
+ // Try to parse as JSON first
249
+ JsonNode jsonArray = objectMapper.readTree(response);
250
+ if (jsonArray.isArray()) {
251
+ for (JsonNode item : jsonArray) {
252
+ LabelSuggestion suggestion = LabelSuggestion.builder()
253
+ .label(item.path("label").asText())
254
+ .confidence(item.path("confidence").asDouble(0.5))
255
+ .reasoning(item.path("reasoning").asText())
256
+ .category(item.path("category").asText())
257
+ .build();
258
+ suggestions.add(suggestion);
259
+ }
260
+ return suggestions;
261
+ }
262
+ } catch (JsonProcessingException e) {
263
+ log.debug("Response is not valid JSON, trying text parsing: {}", e.getMessage());
264
+ }
265
+
266
+ // Fallback: parse as plain text
267
+ return parseLabelsFromText(response);
268
+ }
269
+
270
+ private List<LabelSuggestion> parseLabelsFromText(String response) {
271
+ List<LabelSuggestion> suggestions = new ArrayList<>();
272
+
273
+ // Look for patterns like "- Label" or "1. Label" or just lines with labels
274
+ Pattern labelPattern = Pattern.compile("(?:[-*]|\\d+\\.)\\s*([A-Za-z_][A-Za-z0-9_\\s]*)", Pattern.MULTILINE);
275
+ Matcher matcher = labelPattern.matcher(response);
276
+
277
+ while (matcher.find()) {
278
+ String label = matcher.group(1).trim();
279
+ suggestions.add(LabelSuggestion.builder()
280
+ .label(label)
281
+ .confidence(0.7) // Default confidence for text parsing
282
+ .reasoning("Extracted from text response")
283
+ .category("UNKNOWN")
284
+ .build());
285
+ }
286
+
287
+ return suggestions;
288
+ }
289
+
290
+ private double calculateOverallConfidence(List<LabelSuggestion> suggestions) {
291
+ if (suggestions.isEmpty()) {
292
+ return 0.0;
293
+ }
294
+
295
+ return suggestions.stream()
296
+ .mapToDouble(s -> s.getConfidence() != null ? s.getConfidence() : 0.5)
297
+ .average()
298
+ .orElse(0.0);
299
+ }
300
+
301
+ private boolean isValidOpenAiModel(String modelName) {
302
+ Set<String> validModels = Set.of(
303
+ "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613",
304
+ "gpt-4", "gpt-4-0613", "gpt-4-32k", "gpt-4-32k-0613", "gpt-4-turbo", "gpt-4o"
305
+ );
306
+ return validModels.contains(modelName);
307
+ }
308
+
309
+ private Double getDoubleParameter(MLConfigRequest mlConfig, String key, Double defaultValue) {
310
+ if (mlConfig.getAdditionalParameters() != null && mlConfig.getAdditionalParameters().containsKey(key)) {
311
+ Object value = mlConfig.getAdditionalParameters().get(key);
312
+ if (value instanceof Number) {
313
+ return ((Number) value).doubleValue();
314
+ }
315
+ }
316
+ return defaultValue;
317
+ }
318
+
319
+ private Integer getIntegerParameter(MLConfigRequest mlConfig, String key, Integer defaultValue) {
320
+ if (mlConfig.getAdditionalParameters() != null && mlConfig.getAdditionalParameters().containsKey(key)) {
321
+ Object value = mlConfig.getAdditionalParameters().get(key);
322
+ if (value instanceof Number) {
323
+ return ((Number) value).intValue();
324
+ }
325
+ }
326
+ return defaultValue;
327
+ }
328
+ }
src/main/java/com/dalab/autolabel/llm/model/LLMResponse.java ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.model;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Builder;
5
+ import lombok.Data;
6
+ import lombok.NoArgsConstructor;
7
+
8
+ import java.time.LocalDateTime;
9
+ import java.util.List;
10
+ import java.util.Map;
11
+
12
+ /**
13
+ * Represents a complete response from an LLM including suggestions and metadata.
14
+ */
15
+ @Data
16
+ @Builder
17
+ @NoArgsConstructor
18
+ @AllArgsConstructor
19
+ public class LLMResponse {
20
+
21
+ /**
22
+ * The input content that was processed.
23
+ */
24
+ private String inputContent;
25
+
26
+ /**
27
+ * List of label suggestions with confidence scores.
28
+ */
29
+ private List<LabelSuggestion> suggestions;
30
+
31
+ /**
32
+ * Overall confidence in the response quality.
33
+ */
34
+ private Double overallConfidence;
35
+
36
+ /**
37
+ * Processing time in milliseconds.
38
+ */
39
+ private Long processingTimeMs;
40
+
41
+ /**
42
+ * Timestamp when the response was generated.
43
+ */
44
+ private LocalDateTime timestamp;
45
+
46
+ /**
47
+ * The LLM provider used.
48
+ */
49
+ private String provider;
50
+
51
+ /**
52
+ * The model name used.
53
+ */
54
+ private String modelName;
55
+
56
+ /**
57
+ * Number of tokens used in the request.
58
+ */
59
+ private Integer tokensUsed;
60
+
61
+ /**
62
+ * Cost of the request (if applicable).
63
+ */
64
+ private Double estimatedCost;
65
+
66
+ /**
67
+ * Any additional metadata from the LLM response.
68
+ */
69
+ private Map<String, Object> metadata;
70
+
71
+ /**
72
+ * Error message if the request failed.
73
+ */
74
+ private String errorMessage;
75
+
76
+ /**
77
+ * Whether the response was successful.
78
+ */
79
+ private Boolean successful;
80
+ }
src/main/java/com/dalab/autolabel/llm/model/LabelSuggestion.java ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.llm.model;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Builder;
5
+ import lombok.Data;
6
+ import lombok.NoArgsConstructor;
7
+
8
+ /**
9
+ * Represents a label suggestion from an LLM with associated confidence score.
10
+ */
11
+ @Data
12
+ @Builder
13
+ @NoArgsConstructor
14
+ @AllArgsConstructor
15
+ public class LabelSuggestion {
16
+
17
+ /**
18
+ * The suggested label text.
19
+ */
20
+ private String label;
21
+
22
+ /**
23
+ * Confidence score for this suggestion (0.0 to 1.0).
24
+ */
25
+ private Double confidence;
26
+
27
+ /**
28
+ * Optional reasoning or explanation for this suggestion.
29
+ */
30
+ private String reasoning;
31
+
32
+ /**
33
+ * Category or type of this label (e.g., "PII", "BUSINESS", "TECHNICAL").
34
+ */
35
+ private String category;
36
+ }
src/main/java/com/dalab/autolabel/mapper/LabelingJobMapper.java ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.mapper;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.LabelingJobStatusResponse;
4
+ import com.dalab.autolabel.client.rest.dto.LabelingJobListResponse;
5
+ import com.dalab.autolabel.entity.LabelingJobEntity;
6
+ import org.mapstruct.Mapper;
7
+ import org.mapstruct.Mapping;
8
+ import org.mapstruct.factory.Mappers;
9
+ import org.springframework.data.domain.Page;
10
+
11
+ import java.util.List;
12
+
13
+ @Mapper(componentModel = "spring")
14
+ public interface LabelingJobMapper {
15
+
16
+ LabelingJobMapper INSTANCE = Mappers.getMapper(LabelingJobMapper.class);
17
+
18
+ @Mapping(source = "jobId", target = "jobId")
19
+ LabelingJobStatusResponse toStatusResponse(LabelingJobEntity entity);
20
+
21
+ List<LabelingJobStatusResponse> toStatusResponseList(List<LabelingJobEntity> entities);
22
+
23
+ default LabelingJobListResponse toJobListResponse(Page<LabelingJobEntity> page) {
24
+ if (page == null) {
25
+ return null;
26
+ }
27
+ return LabelingJobListResponse.builder()
28
+ .jobs(toStatusResponseList(page.getContent()))
29
+ .pageNumber(page.getNumber())
30
+ .pageSize(page.getSize())
31
+ .totalElements(page.getTotalElements())
32
+ .totalPages(page.getTotalPages())
33
+ .last(page.isLast())
34
+ .build();
35
+ }
36
+ // Add mappings from LabelingJobRequest to LabelingJobEntity if needed for creation
37
+ // Add mappings from LabelingFeedbackRequest to LabelingFeedbackEntity
38
+ }
src/main/java/com/dalab/autolabel/repository/LabelingFeedbackRepository.java ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.repository;
2
+
3
+ import com.dalab.autolabel.entity.LabelingFeedbackEntity;
4
+ import org.springframework.data.jpa.repository.JpaRepository;
5
+ import org.springframework.stereotype.Repository;
6
+
7
+ import java.util.List;
8
+
9
+ /**
10
+ * Spring Data JPA repository for the {@link LabelingFeedbackEntity} entity.
11
+ */
12
+ @Repository
13
+ public interface LabelingFeedbackRepository extends JpaRepository<LabelingFeedbackEntity, String> {
14
+
15
+ List<LabelingFeedbackEntity> findByAssetId(String assetId);
16
+ List<LabelingFeedbackEntity> findByLabelingJobId(String labelingJobId);
17
+ List<LabelingFeedbackEntity> findByUserId(String userId);
18
+
19
+ }
src/main/java/com/dalab/autolabel/repository/LabelingJobRepository.java ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.repository;
2
+
3
+ import com.dalab.autolabel.entity.LabelingJobEntity;
4
+ import org.springframework.data.jpa.repository.JpaRepository;
5
+ import org.springframework.stereotype.Repository;
6
+
7
+ /**
8
+ * Spring Data JPA repository for the {@link LabelingJobEntity} entity.
9
+ */
10
+ @Repository
11
+ public interface LabelingJobRepository extends JpaRepository<LabelingJobEntity, String> {
12
+ // Custom query methods can be added here if needed, e.g., findByStatus
13
+ }
src/main/java/com/dalab/autolabel/security/SecurityUtils.java ADDED
@@ -0,0 +1 @@
 
 
1
+
src/main/java/com/dalab/autolabel/service/AutoLabelingService.java ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service;
2
+
3
+ import java.time.LocalDateTime;
4
+ import java.util.ArrayList;
5
+ import java.util.List;
6
+ import java.util.Map;
7
+ import java.util.UUID;
8
+ import java.util.stream.Collectors;
9
+
10
+ import org.slf4j.Logger;
11
+ import org.slf4j.LoggerFactory;
12
+ import org.springframework.beans.factory.annotation.Autowired;
13
+ import org.springframework.stereotype.Service;
14
+
15
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackRequest;
16
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackResponse;
17
+ import com.dalab.autolabel.client.rest.dto.LabelingJobRequest;
18
+ import com.dalab.autolabel.client.rest.dto.LabelingJobResponse;
19
+ import com.dalab.autolabel.client.rest.dto.LabelingJobStatusResponse;
20
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
21
+ import com.dalab.autolabel.llm.client.ILLMClient;
22
+ import com.dalab.autolabel.llm.client.LLMClientFactory;
23
+ import com.dalab.autolabel.llm.model.LabelSuggestion;
24
+
25
+ /**
26
+ * Core auto-labeling service that orchestrates the LLM-powered labeling workflow.
27
+ * Handles job creation, execution, feedback processing, and active learning.
28
+ */
29
+ @Service
30
+ public class AutoLabelingService {
31
+
32
+ private static final Logger log = LoggerFactory.getLogger(AutoLabelingService.class);
33
+
34
+ private final LLMClientFactory llmClientFactory;
35
+ // TODO: Add repositories and other services when implemented
36
+ // private final LabelingJobRepository labelingJobRepository;
37
+ // private final FeedbackRepository feedbackRepository;
38
+ // private final CatalogServiceClient catalogServiceClient;
39
+
40
+ // Global ML configuration (can be updated via config API)
41
+ private MLConfigRequest globalMlConfig;
42
+
43
+ @Autowired
44
+ public AutoLabelingService(LLMClientFactory llmClientFactory) {
45
+ this.llmClientFactory = llmClientFactory;
46
+ }
47
+
48
+ /**
49
+ * Creates and processes a labeling job for one or more assets.
50
+ *
51
+ * @param request The labeling job request containing scope and configuration
52
+ * @return The labeling job response with job status
53
+ */
54
+ public LabelingJobResponse createLabelingJob(LabelingJobRequest request) {
55
+ log.info("Creating labeling job: {}", request.getJobName());
56
+
57
+ long startTime = System.currentTimeMillis();
58
+ String jobId = UUID.randomUUID().toString();
59
+
60
+ try {
61
+ // Validate the request
62
+ validateLabelingRequest(request);
63
+
64
+ // Get ML configuration (use override or global default)
65
+ MLConfigRequest mlConfig = request.getOverrideMlConfig() != null
66
+ ? request.getOverrideMlConfig()
67
+ : globalMlConfig;
68
+
69
+ if (mlConfig == null) {
70
+ throw new IllegalArgumentException("No ML configuration available");
71
+ }
72
+
73
+ // Get the appropriate LLM client
74
+ ILLMClient llmClient = llmClientFactory.getClient(mlConfig);
75
+
76
+ // Get assets to process based on scope
77
+ List<String> assetIds = getAssetsFromScope(request.getScope());
78
+
79
+ if (assetIds.isEmpty()) {
80
+ return LabelingJobResponse.builder()
81
+ .jobId(jobId)
82
+ .status("COMPLETED")
83
+ .submittedAt(LocalDateTime.now())
84
+ .createdAt(LocalDateTime.now())
85
+ .completedAt(LocalDateTime.now())
86
+ .processingTimeMs(System.currentTimeMillis() - startTime)
87
+ .message("No assets found matching the specified scope")
88
+ .build();
89
+ }
90
+
91
+ // For single asset, process immediately
92
+ if (assetIds.size() == 1) {
93
+ String assetId = assetIds.get(0);
94
+
95
+ // Get asset content for LLM processing
96
+ String assetContent = getAssetContent(assetId);
97
+
98
+ // Get label suggestions from LLM
99
+ List<LabelSuggestion> suggestions = llmClient.suggestLabelsWithConfidence(assetContent, mlConfig);
100
+
101
+ // Filter suggestions based on confidence thresholds
102
+ List<LabelSuggestion> filteredSuggestions = filterSuggestionsByConfidence(suggestions, mlConfig);
103
+
104
+ return LabelingJobResponse.builder()
105
+ .jobId(jobId)
106
+ .assetId(assetId)
107
+ .status("COMPLETED")
108
+ .submittedAt(LocalDateTime.now())
109
+ .createdAt(LocalDateTime.now())
110
+ .completedAt(LocalDateTime.now())
111
+ .processingTimeMs(System.currentTimeMillis() - startTime)
112
+ .message("Labeling completed successfully")
113
+ .suggestions(convertToLabelSuggestionDTOs(filteredSuggestions))
114
+ .build();
115
+ } else {
116
+ // For multiple assets, return job submitted status
117
+ // TODO: Implement async processing for large batches
118
+ return LabelingJobResponse.builder()
119
+ .jobId(jobId)
120
+ .status("SUBMITTED")
121
+ .submittedAt(LocalDateTime.now())
122
+ .createdAt(LocalDateTime.now())
123
+ .message("Job submitted for processing " + assetIds.size() + " assets")
124
+ .build();
125
+ }
126
+
127
+ } catch (Exception e) {
128
+ log.error("Failed to create labeling job {}: {}", request.getJobName(), e.getMessage(), e);
129
+
130
+ return LabelingJobResponse.builder()
131
+ .jobId(jobId)
132
+ .status("FAILED")
133
+ .submittedAt(LocalDateTime.now())
134
+ .createdAt(LocalDateTime.now())
135
+ .completedAt(LocalDateTime.now())
136
+ .processingTimeMs(System.currentTimeMillis() - startTime)
137
+ .errorMessage("Failed to process labeling job: " + e.getMessage())
138
+ .build();
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Processes multiple labeling jobs in batch for efficiency.
144
+ *
145
+ * @param jobRequests List of labeling job requests
146
+ * @return List of labeling job responses
147
+ */
148
+ public List<LabelingJobResponse> createBatchLabelingJobs(List<LabelingJobRequest> jobRequests) {
149
+ log.info("Processing batch of {} labeling jobs", jobRequests.size());
150
+
151
+ List<LabelingJobResponse> allResponses = new ArrayList<>();
152
+
153
+ // Group requests by ML configuration to process efficiently
154
+ Map<MLConfigRequest, List<LabelingJobRequest>> configGroups = jobRequests.stream()
155
+ .collect(Collectors.groupingBy(request ->
156
+ request.getOverrideMlConfig() != null ? request.getOverrideMlConfig() : globalMlConfig));
157
+
158
+ for (Map.Entry<MLConfigRequest, List<LabelingJobRequest>> entry : configGroups.entrySet()) {
159
+ MLConfigRequest mlConfig = entry.getKey();
160
+ List<LabelingJobRequest> requests = entry.getValue();
161
+
162
+ try {
163
+ ILLMClient llmClient = llmClientFactory.getClient(mlConfig);
164
+
165
+ for (LabelingJobRequest request : requests) {
166
+ try {
167
+ LabelingJobResponse response = createLabelingJob(request);
168
+ allResponses.add(response);
169
+ } catch (Exception e) {
170
+ log.error("Failed to process job {}: {}", request.getJobName(), e.getMessage(), e);
171
+
172
+ LabelingJobResponse errorResponse = LabelingJobResponse.builder()
173
+ .jobId(UUID.randomUUID().toString())
174
+ .status("FAILED")
175
+ .submittedAt(LocalDateTime.now())
176
+ .createdAt(LocalDateTime.now())
177
+ .completedAt(LocalDateTime.now())
178
+ .errorMessage("Job processing failed: " + e.getMessage())
179
+ .build();
180
+ allResponses.add(errorResponse);
181
+ }
182
+ }
183
+
184
+ } catch (Exception e) {
185
+ log.error("Failed to process batch with ML config {}: {}", mlConfig.getProviderType(), e.getMessage(), e);
186
+
187
+ // Create error responses for all requests in this batch
188
+ for (LabelingJobRequest request : requests) {
189
+ LabelingJobResponse errorResponse = LabelingJobResponse.builder()
190
+ .jobId(UUID.randomUUID().toString())
191
+ .status("FAILED")
192
+ .submittedAt(LocalDateTime.now())
193
+ .createdAt(LocalDateTime.now())
194
+ .completedAt(LocalDateTime.now())
195
+ .errorMessage("Batch processing failed: " + e.getMessage())
196
+ .build();
197
+ allResponses.add(errorResponse);
198
+ }
199
+ }
200
+ }
201
+
202
+ return allResponses;
203
+ }
204
+
205
+ /**
206
+ * Processes feedback for active learning and model improvement.
207
+ *
208
+ * @param feedback The feedback request containing user corrections
209
+ * @return The feedback response
210
+ */
211
+ public LabelingFeedbackResponse processFeedback(LabelingFeedbackRequest feedback) {
212
+ log.info("Processing feedback for asset: {}", feedback.getAssetId());
213
+
214
+ try {
215
+ // TODO: Store feedback in database for active learning
216
+ // feedbackRepository.save(LabelingFeedbackEntity.fromDTO(feedback));
217
+
218
+ // TODO: Update asset labels via catalog service
219
+ // if (feedback.getCorrectedLabels() != null) {
220
+ // catalogServiceClient.updateAssetLabels(feedback.getAssetId(), feedback.getCorrectedLabels());
221
+ // }
222
+
223
+ // TODO: Trigger model retraining if enough feedback accumulated
224
+ // checkAndTriggerActivelearning();
225
+
226
+ return LabelingFeedbackResponse.builder()
227
+ .feedbackId(UUID.randomUUID().toString())
228
+ .assetId(feedback.getAssetId())
229
+ .status("PROCESSED")
230
+ .message("Feedback processed successfully")
231
+ .processedAt(LocalDateTime.now())
232
+ .build();
233
+
234
+ } catch (Exception e) {
235
+ log.error("Failed to process feedback for asset {}: {}", feedback.getAssetId(), e.getMessage(), e);
236
+
237
+ return LabelingFeedbackResponse.builder()
238
+ .feedbackId(UUID.randomUUID().toString())
239
+ .assetId(feedback.getAssetId())
240
+ .status("FAILED")
241
+ .message("Failed to process feedback: " + e.getMessage())
242
+ .processedAt(LocalDateTime.now())
243
+ .build();
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Updates ML configuration and validates it.
249
+ *
250
+ * @param mlConfig The new ML configuration
251
+ * @return true if configuration is valid and updated, false otherwise
252
+ */
253
+ public boolean updateMLConfiguration(MLConfigRequest mlConfig) {
254
+ log.info("Updating ML configuration for provider: {}", mlConfig.getProviderType());
255
+
256
+ try {
257
+ // Validate configuration
258
+ boolean isValid = llmClientFactory.validateConfiguration(mlConfig);
259
+ if (!isValid) {
260
+ log.warn("ML configuration validation failed for provider: {}", mlConfig.getProviderType());
261
+ return false;
262
+ }
263
+
264
+ // Test connection
265
+ boolean connectionOk = llmClientFactory.testConnection(mlConfig);
266
+ if (!connectionOk) {
267
+ log.warn("ML configuration connection test failed for provider: {}", mlConfig.getProviderType());
268
+ return false;
269
+ }
270
+
271
+ // TODO: Store configuration in database
272
+ // configurationRepository.save(mlConfig);
273
+
274
+ log.info("ML configuration updated successfully for provider: {}", mlConfig.getProviderType());
275
+ return true;
276
+
277
+ } catch (Exception e) {
278
+ log.error("Failed to update ML configuration: {}", e.getMessage(), e);
279
+ return false;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Gets the status of a labeling job.
285
+ *
286
+ * @param jobId The job ID
287
+ * @return The job status response
288
+ */
289
+ public LabelingJobStatusResponse getJobStatus(UUID jobId) {
290
+ log.debug("Getting status for job: {}", jobId);
291
+
292
+ // TODO: Implement job status tracking in database
293
+ // For now, return a mock response
294
+ return LabelingJobStatusResponse.builder()
295
+ .jobId(jobId.toString())
296
+ .status("COMPLETED")
297
+ .progressPercentage(100.0)
298
+ .startedAt(LocalDateTime.now().minusMinutes(5))
299
+ .completedAt(LocalDateTime.now())
300
+ .build();
301
+ }
302
+
303
+ /**
304
+ * Gets all available LLM providers and their capabilities.
305
+ *
306
+ * @return Map of provider capabilities
307
+ */
308
+ public Map<String, Map<String, Object>> getProviderCapabilities() {
309
+ return llmClientFactory.getAllProviderCapabilities();
310
+ }
311
+
312
+ // Private helper methods
313
+
314
+ private void validateLabelingRequest(LabelingJobRequest request) {
315
+ if (request == null) {
316
+ throw new IllegalArgumentException("Labeling request cannot be null");
317
+ }
318
+ if (request.getJobName() == null || request.getJobName().trim().isEmpty()) {
319
+ throw new IllegalArgumentException("Job name cannot be null or empty");
320
+ }
321
+ if (request.getScope() == null) {
322
+ throw new IllegalArgumentException("Labeling scope cannot be null");
323
+ }
324
+
325
+ // Validate that at least one asset selection method is provided
326
+ if ((request.getScope().getAssetIds() == null || request.getScope().getAssetIds().isEmpty()) &&
327
+ request.getScope().getCloudConnectionId() == null &&
328
+ (request.getScope().getAssetCriteria() == null || request.getScope().getAssetCriteria().isEmpty()) &&
329
+ request.getScope().getAssetGroupId() == null) {
330
+ throw new IllegalArgumentException("At least one asset selection method must be provided in scope");
331
+ }
332
+ }
333
+
334
+ private String prepareAssetContent(LabelingJobRequest request) {
335
+ // This method is no longer used with the new DTO structure
336
+ // Asset content is now retrieved per asset ID via getAssetContent()
337
+ return "Asset content preparation moved to getAssetContent() method";
338
+ }
339
+
340
+ private List<LabelSuggestion> filterSuggestionsByConfidence(List<LabelSuggestion> suggestions, MLConfigRequest mlConfig) {
341
+ if (mlConfig.getConfidenceConfig() == null || mlConfig.getConfidenceConfig().getMinThreshold() == null) {
342
+ return suggestions; // No filtering
343
+ }
344
+
345
+ double minThreshold = mlConfig.getConfidenceConfig().getMinThreshold();
346
+
347
+ return suggestions.stream()
348
+ .filter(suggestion -> suggestion.getConfidence() != null && suggestion.getConfidence() >= minThreshold)
349
+ .collect(Collectors.toList());
350
+ }
351
+
352
+ /**
353
+ * Get list of asset IDs from the labeling scope.
354
+ */
355
+ private List<String> getAssetsFromScope(LabelingJobRequest.LabelingScope scope) {
356
+ List<String> assetIds = new ArrayList<>();
357
+
358
+ if (scope.getAssetIds() != null && !scope.getAssetIds().isEmpty()) {
359
+ assetIds.addAll(scope.getAssetIds());
360
+ }
361
+
362
+ // TODO: Implement other scope types
363
+ // if (scope.getCloudConnectionId() != null) {
364
+ // assetIds.addAll(assetCatalogClient.getAssetsByConnectionId(scope.getCloudConnectionId()));
365
+ // }
366
+ // if (scope.getAssetCriteria() != null) {
367
+ // assetIds.addAll(assetCatalogClient.getAssetsByCriteria(scope.getAssetCriteria()));
368
+ // }
369
+
370
+ return assetIds;
371
+ }
372
+
373
+ /**
374
+ * Get asset content for LLM processing.
375
+ */
376
+ private String getAssetContent(String assetId) {
377
+ // TODO: Implement actual asset content retrieval
378
+ // For now return a placeholder
379
+ return "Asset content for " + assetId + " - metadata, schema, and sample data would go here";
380
+ }
381
+
382
+ /**
383
+ * Convert LLM label suggestions to DTOs.
384
+ */
385
+ private List<LabelingJobResponse.LabelSuggestionDTO> convertToLabelSuggestionDTOs(List<LabelSuggestion> suggestions) {
386
+ return suggestions.stream()
387
+ .map(suggestion -> LabelingJobResponse.LabelSuggestionDTO.builder()
388
+ .labelName(suggestion.getLabel())
389
+ .confidence(suggestion.getConfidence())
390
+ .reasoning(suggestion.getReasoning())
391
+ .category(suggestion.getCategory())
392
+ .build())
393
+ .collect(Collectors.toList());
394
+ }
395
+ }
src/main/java/com/dalab/autolabel/service/ILLMIntegrationService.java ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
4
+ import java.util.List;
5
+
6
+ /**
7
+ * Service interface for integrating with various LLM clients.
8
+ */
9
+ public interface ILLMIntegrationService {
10
+
11
+ /**
12
+ * Suggests labels for a given asset content using the configured LLM.
13
+ *
14
+ * @param assetContent The content or metadata of the asset to label.
15
+ * @param mlConfig The ML configuration specifying which LLM to use and its parameters.
16
+ * @return A list of suggested labels.
17
+ * @throws Exception if there is an error during LLM interaction or if the client is not found.
18
+ */
19
+ List<String> suggestLabelsForAsset(String assetContent, MLConfigRequest mlConfig) throws Exception;
20
+
21
+ }
src/main/java/com/dalab/autolabel/service/ILabelingJobService.java ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.LabelingJobRequest;
4
+ import com.dalab.autolabel.client.rest.dto.LabelingJobResponse;
5
+ import com.dalab.autolabel.client.rest.dto.LabelingJobStatusResponse;
6
+ import com.dalab.autolabel.client.rest.dto.LabelingJobListResponse;
7
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackRequest;
8
+ import com.dalab.autolabel.client.rest.dto.LabelingFeedbackResponse;
9
+ import com.dalab.autolabel.exception.AutoLabelingException;
10
+ import org.springframework.data.domain.Pageable;
11
+
12
+ /**
13
+ * Service interface for managing auto-labeling jobs.
14
+ */
15
+ public interface ILabelingJobService {
16
+
17
+ /**
18
+ * Submits a new auto-labeling job.
19
+ *
20
+ * @param request The labeling job request DTO.
21
+ * @return A response DTO containing the job ID and initial status.
22
+ * @throws AutoLabelingException if ML config is not found or other issues.
23
+ */
24
+ LabelingJobResponse submitLabelingJob(LabelingJobRequest request) throws AutoLabelingException;
25
+
26
+ /**
27
+ * Retrieves the status of a specific labeling job.
28
+ *
29
+ * @param jobId The ID of the job.
30
+ * @return A response DTO containing the job status, or null if not found.
31
+ */
32
+ LabelingJobStatusResponse getJobStatus(String jobId);
33
+
34
+ /**
35
+ * Lists all labeling jobs, with optional pagination.
36
+ *
37
+ * @param pageable Pagination information.
38
+ * @return A paginated list of job statuses.
39
+ */
40
+ LabelingJobListResponse listJobs(Pageable pageable);
41
+
42
+ /**
43
+ * Processes user feedback on suggested labels for an asset.
44
+ *
45
+ * @param request The labeling feedback request DTO.
46
+ * @return A response DTO indicating the result of processing the feedback.
47
+ * @throws AutoLabelingException if there is an issue processing the feedback.
48
+ */
49
+ LabelingFeedbackResponse processLabelingFeedback(LabelingFeedbackRequest request) throws AutoLabelingException;
50
+
51
+ // Potentially add other methods like listJobs, cancelJob, etc. in the future.
52
+ }
src/main/java/com/dalab/autolabel/service/IMLConfigService.java ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
4
+
5
+ /**
6
+ * Service interface for managing ML configurations for auto-labeling.
7
+ */
8
+ public interface IMLConfigService {
9
+
10
+ /**
11
+ * Updates the ML configuration.
12
+ *
13
+ * @param mlConfigRequest The ML configuration request DTO.
14
+ */
15
+ void updateMlConfig(MLConfigRequest mlConfigRequest);
16
+
17
+ /**
18
+ * Retrieves the current ML configuration.
19
+ *
20
+ * @return The current MLConfigRequest, or null if not configured.
21
+ */
22
+ MLConfigRequest getMlConfig();
23
+
24
+ }
src/main/java/com/dalab/autolabel/service/impl/InMemoryMLConfigService.java ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service.impl;
2
+
3
+ import org.springframework.stereotype.Service;
4
+
5
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
6
+ import com.dalab.autolabel.service.IMLConfigService;
7
+
8
+ import lombok.extern.slf4j.Slf4j;
9
+
10
+ /**
11
+ * In-memory implementation of IMLConfigService.
12
+ * This is a basic implementation and should be replaced with a persistent store in a production environment.
13
+ */
14
+ @Service
15
+ @Slf4j
16
+ public class InMemoryMLConfigService implements IMLConfigService {
17
+
18
+ private MLConfigRequest currentConfig;
19
+
20
+ /**
21
+ * Updates the ML configuration.
22
+ *
23
+ * @param mlConfigRequest The ML configuration request DTO.
24
+ * @throws IllegalArgumentException if mlConfigRequest is null
25
+ */
26
+ @Override
27
+ public void updateMlConfig(MLConfigRequest mlConfigRequest) {
28
+ if (mlConfigRequest == null) {
29
+ throw new IllegalArgumentException("ML configuration request cannot be null");
30
+ }
31
+
32
+ log.info("Updating ML configuration: {}", mlConfigRequest);
33
+ // In a real application, perform validation and secure storage of API keys.
34
+ this.currentConfig = mlConfigRequest;
35
+ log.info("ML configuration updated successfully.");
36
+ // TODO: Persist this configuration to a database or a secure configuration management system.
37
+ // TODO: Implement secure handling of sensitive data like API keys (e.g., using Spring Vault).
38
+ }
39
+
40
+ /**
41
+ * Retrieves the current ML configuration.
42
+ *
43
+ * @return The current MLConfigRequest, or null if not configured.
44
+ */
45
+ @Override
46
+ public MLConfigRequest getMlConfig() {
47
+ if (this.currentConfig == null) {
48
+ log.warn("ML configuration has not been set up yet.");
49
+ // Optionally, return a default configuration or throw an exception.
50
+ }
51
+ return this.currentConfig;
52
+ }
53
+ }
src/main/java/com/dalab/autolabel/service/impl/LLMIntegrationServiceImpl.java ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service.impl;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
4
+ import com.dalab.autolabel.llm.client.ILLMClient;
5
+ import com.dalab.autolabel.service.ILLMIntegrationService;
6
+ import lombok.extern.slf4j.Slf4j;
7
+ import org.springframework.beans.factory.annotation.Autowired;
8
+ import org.springframework.stereotype.Service;
9
+
10
+ import java.util.List;
11
+ import java.util.Map;
12
+ import java.util.Optional;
13
+
14
+ @Service
15
+ @Slf4j
16
+ public class LLMIntegrationServiceImpl implements ILLMIntegrationService {
17
+
18
+ private final Map<String, ILLMClient> llmClients; // Injected map of all ILLMClient beans
19
+
20
+ @Autowired
21
+ public LLMIntegrationServiceImpl(Map<String, ILLMClient> llmClients) {
22
+ this.llmClients = llmClients;
23
+ log.info("Initialized LLMIntegrationService with available clients: {}", llmClients.keySet());
24
+ }
25
+
26
+ @Override
27
+ public List<String> suggestLabelsForAsset(String assetContent, MLConfigRequest mlConfig) throws Exception {
28
+ if (mlConfig == null || mlConfig.getProviderType() == null) {
29
+ log.error("MLConfig or ProviderType is null. Cannot determine LLM client.");
30
+ // Fallback to a default mock client if no provider type is specified or use global default
31
+ ILLMClient defaultClient = llmClients.get("mockLLMClient"); // Bean name of MockLLMClient
32
+ if (defaultClient != null) {
33
+ log.warn("Using default MOCK LLM client as provider type was not specified in MLConfig.");
34
+ return defaultClient.suggestLabels(assetContent, mlConfig != null ? mlConfig : new MLConfigRequest()); // Pass a default config if null
35
+ } else {
36
+ throw new IllegalStateException("MLConfig.providerType is null and no default MOCK LLM client is available.");
37
+ }
38
+ }
39
+
40
+ // Construct bean name based on provider type, e.g., "openaiLLMClient", "geminiLLMClient"
41
+ // This assumes ILLMClient beans are named like "[providerType]LLMClient"
42
+ String clientBeanName = mlConfig.getProviderType().toLowerCase() + "LLMClient";
43
+ ILLMClient client = llmClients.get(clientBeanName);
44
+
45
+ if (client == null) {
46
+ // Fallback: try finding client by provider name if naming convention isn't strict
47
+ Optional<ILLMClient> foundClient = llmClients.values().stream()
48
+ .filter(c -> c.getProviderName().equalsIgnoreCase(mlConfig.getProviderType()))
49
+ .findFirst();
50
+ if (foundClient.isPresent()){
51
+ client = foundClient.get();
52
+ log.warn("LLM client for provider '{}' found by provider name, not bean naming convention.", mlConfig.getProviderType());
53
+ } else {
54
+ log.error("No LLM client found for provider type: {}. Available clients: {}. Attempted bean name: {}",
55
+ mlConfig.getProviderType(), llmClients.keySet(), clientBeanName);
56
+ // Fallback to mock if specific client not found but mock exists
57
+ ILLMClient mockClient = llmClients.get("mockLLMClient");
58
+ if (mockClient != null) {
59
+ log.warn("Falling back to MOCK LLM client as client for '{}' was not found.", mlConfig.getProviderType());
60
+ return mockClient.suggestLabels(assetContent, mlConfig);
61
+ }
62
+ throw new IllegalArgumentException("No LLM client configured for provider: " + mlConfig.getProviderType() + " and no mock fallback.");
63
+ }
64
+ }
65
+
66
+ log.info("Using LLM client: {}", client.getClass().getSimpleName());
67
+ return client.suggestLabels(assetContent, mlConfig);
68
+ }
69
+ }
src/main/java/com/dalab/autolabel/service/impl/LabelingJobServiceImpl.java ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service.impl;
2
+
3
+ import com.dalab.autolabel.client.dto.AssetIdentifier;
4
+ import com.dalab.autolabel.client.feign.AssetCatalogClient;
5
+ import com.dalab.autolabel.client.rest.dto.*;
6
+ import com.dalab.autolabel.entity.LabelingFeedbackEntity;
7
+ import com.dalab.autolabel.entity.LabelingJobEntity;
8
+ import com.dalab.autolabel.exception.AutoLabelingException;
9
+ import com.dalab.autolabel.mapper.LabelingJobMapper;
10
+ import com.dalab.autolabel.repository.LabelingFeedbackRepository;
11
+ import com.dalab.autolabel.repository.LabelingJobRepository;
12
+ import com.dalab.autolabel.service.IMLConfigService;
13
+ import com.dalab.autolabel.service.ILabelingJobService;
14
+ import com.dalab.autolabel.service.ILLMIntegrationService;
15
+ import lombok.RequiredArgsConstructor;
16
+ import lombok.extern.slf4j.Slf4j;
17
+ import org.springframework.data.domain.Page;
18
+ import org.springframework.data.domain.Pageable;
19
+ import org.springframework.scheduling.annotation.Async;
20
+ import org.springframework.stereotype.Service;
21
+ import org.springframework.transaction.annotation.Transactional;
22
+
23
+ import java.time.LocalDateTime;
24
+ import java.util.ArrayList;
25
+ import java.util.List;
26
+ import java.util.UUID;
27
+ import java.util.stream.Collectors;
28
+
29
+ @Service
30
+ @Slf4j
31
+ @RequiredArgsConstructor
32
+ public class LabelingJobServiceImpl implements ILabelingJobService {
33
+
34
+ private final IMLConfigService mlConfigService;
35
+ private final LabelingJobRepository labelingJobRepository;
36
+ private final LabelingFeedbackRepository labelingFeedbackRepository;
37
+ private final LabelingJobMapper labelingJobMapper;
38
+ private final AssetCatalogClient assetCatalogClient;
39
+ private final ILLMIntegrationService llmIntegrationService;
40
+
41
+ @Override
42
+ @Transactional
43
+ public LabelingJobResponse submitLabelingJob(LabelingJobRequest request) throws AutoLabelingException {
44
+ log.info("Received request to submit labeling job: {}", request.getJobName());
45
+
46
+ MLConfigRequest currentMlConfig = request.getOverrideMlConfig() != null ?
47
+ request.getOverrideMlConfig() :
48
+ mlConfigService.getMlConfig();
49
+
50
+ if (currentMlConfig == null) {
51
+ log.error("ML configuration not found. Cannot submit labeling job.");
52
+ throw new AutoLabelingException("Auto-labeling ML configuration is not set. Please configure it via PUT /api/v1/labeling/config/ml.");
53
+ }
54
+ if (currentMlConfig.getProviderType() == null && !"MOCK".equalsIgnoreCase(currentMlConfig.getModelName())) {
55
+ log.warn("MLConfig.providerType is null for job '{}'. LLMIntegrationService might default to mock.", request.getJobName());
56
+ }
57
+
58
+ String jobId = UUID.randomUUID().toString();
59
+ LocalDateTime submittedAt = LocalDateTime.now();
60
+
61
+ LabelingJobEntity jobEntity = LabelingJobEntity.builder()
62
+ .jobId(jobId)
63
+ .jobName(request.getJobName())
64
+ .status(LabelingJobEntity.JobStatus.SUBMITTED)
65
+ .submittedAt(submittedAt)
66
+ .lastUpdatedAt(submittedAt)
67
+ .scope(request.getScope())
68
+ .effectiveMlConfig(currentMlConfig)
69
+ .totalAssetsToProcess(0)
70
+ .assetsProcessed(0)
71
+ .assetsSuccessfullyLabeled(0)
72
+ .assetsFailed(0)
73
+ .progressPercentage(0.0)
74
+ .errorMessages(new ArrayList<>())
75
+ .build();
76
+ labelingJobRepository.save(jobEntity);
77
+
78
+ log.info("Labeling job {} submitted successfully. Job ID: {}", request.getJobName(), jobId);
79
+ processLabelingJobAsync(jobId, request.getJobName(), request.getScope(), currentMlConfig);
80
+
81
+ return LabelingJobResponse.builder()
82
+ .jobId(jobId)
83
+ .status("SUBMITTED")
84
+ .submittedAt(submittedAt)
85
+ .message("Labeling job '" + request.getJobName() + "' submitted successfully.")
86
+ .build();
87
+ }
88
+
89
+ @Override
90
+ @Transactional(readOnly = true)
91
+ public LabelingJobStatusResponse getJobStatus(String jobId) {
92
+ log.debug("Fetching status for job ID: {}", jobId);
93
+ return labelingJobRepository.findById(jobId)
94
+ .map(labelingJobMapper::toStatusResponse)
95
+ .orElse(null);
96
+ }
97
+
98
+ @Override
99
+ @Transactional(readOnly = true)
100
+ public LabelingJobListResponse listJobs(Pageable pageable) {
101
+ log.debug("Fetching list of jobs with pagination: {}", pageable);
102
+ Page<LabelingJobEntity> jobPage = labelingJobRepository.findAll(pageable);
103
+ return labelingJobMapper.toJobListResponse(jobPage);
104
+ }
105
+
106
+ @Async
107
+ @Transactional
108
+ public void processLabelingJobAsync(String jobId, String jobName, LabelingJobRequest.LabelingScope scope, MLConfigRequest mlConfig) {
109
+ log.info("[Job ID: {}] Starting asynchronous processing for job: {}", jobId, jobName);
110
+ updateJobStatus(jobId, LabelingJobEntity.JobStatus.PREPARING_DATA, "Fetching asset list...", null, null, null, null, 0.05);
111
+
112
+ List<AssetIdentifier> targetAssets;
113
+ try {
114
+ targetAssets = resolveAssetIdentifiers(scope);
115
+ } catch (Exception e) {
116
+ log.error("[Job ID: {}] Failed to resolve asset identifiers: {}", jobId, e.getMessage(), e);
117
+ updateJobStatus(jobId, LabelingJobEntity.JobStatus.FAILED, "Failed to resolve asset scope.", 0,0,0,0, 1.0);
118
+ labelingJobRepository.findById(jobId).ifPresent(job -> job.setCompletedAt(LocalDateTime.now()));
119
+ return;
120
+ }
121
+
122
+ if (targetAssets.isEmpty()) {
123
+ log.warn("[Job ID: {}] No assets found for the given scope.", jobId);
124
+ updateJobStatus(jobId, LabelingJobEntity.JobStatus.FAILED, "No assets found for the given scope.", 0, 0, 0, 0, 1.0);
125
+ labelingJobRepository.findById(jobId).ifPresent(job -> job.setCompletedAt(LocalDateTime.now()));
126
+ return;
127
+ }
128
+ updateJobStatus(jobId, null, "Identified " + targetAssets.size() + " assets for labeling.", targetAssets.size(), 0, 0, 0, 0.1);
129
+ labelingJobRepository.findById(jobId).ifPresent(job -> job.setStartedAt(LocalDateTime.now()));
130
+
131
+ int processed = 0;
132
+ int succeeded = 0;
133
+ int failed = 0;
134
+
135
+ for (AssetIdentifier asset : targetAssets) {
136
+ String assetId = asset.getAssetId();
137
+ try {
138
+ log.info("[Job ID: {}] Processing asset: {}", jobId, assetId);
139
+ updateJobStatus(jobId, LabelingJobEntity.JobStatus.CALLING_LLM, "Processing asset: " + assetId, null, null, null, null, 0.1 + (0.8 * ((double)processed / targetAssets.size())));
140
+
141
+ String assetTechnicalMetadata = "";
142
+ try {
143
+ assetTechnicalMetadata = "This is a mock technical metadata for asset " + assetId + ". Contains keywords: PII, financial, sensitive data.";
144
+ log.info("[Job ID: {}] Mock fetched technical metadata for asset {}: {}", jobId, assetId, assetTechnicalMetadata.substring(0, Math.min(assetTechnicalMetadata.length(), 100)));
145
+ } catch (Exception e) {
146
+ log.warn("[Job ID: {}] Could not fetch technical metadata for asset {}. Proceeding with empty metadata. Error: {}", jobId, assetId, e.getMessage());
147
+ }
148
+
149
+ List<String> suggestedLabels = llmIntegrationService.suggestLabelsForAsset(assetTechnicalMetadata, mlConfig);
150
+ log.info("[Job ID: {}] LLM suggested labels for {}: {}", jobId, assetId, suggestedLabels);
151
+
152
+ log.info("[Job ID: {}] Mock applying labels {} for asset: {}", jobId, suggestedLabels, assetId);
153
+ succeeded++;
154
+
155
+ } catch (InterruptedException e) {
156
+ log.warn("[Job ID: {}] Processing for asset {} interrupted.", jobId, assetId);
157
+ Thread.currentThread().interrupt();
158
+ failed++;
159
+ addErrorMessageToJob(jobId, "Processing interrupted for asset " + assetId);
160
+ } catch (Exception e) {
161
+ log.error("[Job ID: {}] Error processing asset {}: {}", jobId, assetId, e.getMessage(), e);
162
+ failed++;
163
+ addErrorMessageToJob(jobId, "Failed to process asset " + assetId + ": " + e.getMessage());
164
+ }
165
+ processed++;
166
+ updateJobStatus(jobId, null, "Processed asset: " + assetId, null, processed, succeeded, failed, 0.1 + (0.8 * ((double)processed / targetAssets.size())));
167
+ }
168
+
169
+ LabelingJobEntity.JobStatus finalStatus = failed == 0 ? LabelingJobEntity.JobStatus.COMPLETED : (succeeded > 0 ? LabelingJobEntity.JobStatus.PARTIALLY_COMPLETED : LabelingJobEntity.JobStatus.FAILED);
170
+ String finalMessage = String.format("Job %s. Total: %d, Succeeded: %d, Failed: %d.",
171
+ finalStatus.name().toLowerCase(), targetAssets.size(), succeeded, failed);
172
+ updateJobStatus(jobId, finalStatus, finalMessage, null, processed, succeeded, failed, 1.0);
173
+ labelingJobRepository.findById(jobId).ifPresent(job -> job.setCompletedAt(LocalDateTime.now()));
174
+ log.info("[Job ID: {}] Labeling job processing finished with status: {}. Message: {}", jobId, finalStatus, finalMessage);
175
+ }
176
+
177
+ private void updateJobStatus(String jobId, LabelingJobEntity.JobStatus status, String stageDescription,
178
+ Integer totalAssets, Integer processedAssets,
179
+ Integer succeededAssets, Integer failedAssets, Double progress) {
180
+ labelingJobRepository.findById(jobId).ifPresent(job -> {
181
+ if (status != null) job.setStatus(status);
182
+ if (stageDescription != null) job.setCurrentStageDescription(stageDescription);
183
+ if (totalAssets != null) job.setTotalAssetsToProcess(totalAssets);
184
+ if (processedAssets != null) job.setAssetsProcessed(processedAssets);
185
+ if (succeededAssets != null) job.setAssetsSuccessfullyLabeled(succeededAssets);
186
+ if (failedAssets != null) job.setAssetsFailed(failedAssets);
187
+ if (progress != null) job.setProgressPercentage(progress);
188
+ job.setLastUpdatedAt(LocalDateTime.now());
189
+ labelingJobRepository.save(job);
190
+ log.debug("[Job ID: {}] Status updated: {}", jobId, job);
191
+ });
192
+ }
193
+
194
+ private void addErrorMessageToJob(String jobId, String errorMessage) {
195
+ labelingJobRepository.findById(jobId).ifPresent(job -> {
196
+ if(job.getErrorMessages() == null) job.setErrorMessages(new ArrayList<>());
197
+ job.getErrorMessages().add(errorMessage);
198
+ labelingJobRepository.save(job);
199
+ });
200
+ }
201
+
202
+ @Override
203
+ @Transactional
204
+ public LabelingFeedbackResponse processLabelingFeedback(LabelingFeedbackRequest request) throws AutoLabelingException {
205
+ log.info("Received labeling feedback for asset ID: {}, job ID: {}", request.getAssetId(), request.getLabelingJobId());
206
+
207
+ if (request.getFeedbackItems() == null || request.getFeedbackItems().isEmpty()) {
208
+ log.warn("Labeling feedback request for asset {} is empty.", request.getAssetId());
209
+ throw new AutoLabelingException("Feedback items cannot be empty.");
210
+ }
211
+
212
+ String feedbackId = UUID.randomUUID().toString();
213
+ LabelingFeedbackEntity feedbackEntity = LabelingFeedbackEntity.builder()
214
+ .feedbackId(feedbackId)
215
+ .assetId(request.getAssetId())
216
+ .labelingJobId(request.getLabelingJobId())
217
+ .feedbackItems(request.getFeedbackItems())
218
+ .userId(request.getUserId())
219
+ .additionalContext(request.getAdditionalContext())
220
+ .receivedAt(LocalDateTime.now())
221
+ .processingStatus("RECEIVED")
222
+ .build();
223
+ labelingFeedbackRepository.save(feedbackEntity);
224
+
225
+ for (LabelingFeedbackRequest.FeedbackItem item : request.getFeedbackItems()) {
226
+ try {
227
+ switch (item.getType()) {
228
+ case CONFIRMED:
229
+ log.info("[Feedback ID: {}] Asset: {}, Label '{}' confirmed. (Mock catalog update)", feedbackId, request.getAssetId(), item.getSuggestedLabel());
230
+ break;
231
+ case CORRECTED:
232
+ log.info("[Feedback ID: {}] Asset: {}, Label '{}' corrected to '{}'. (Mock catalog update)",
233
+ feedbackId, request.getAssetId(), item.getSuggestedLabel(), item.getCorrectedLabel());
234
+ break;
235
+ case REJECTED:
236
+ log.info("[Feedback ID: {}] Asset: {}, Label '{}' rejected. (Mock catalog update)", feedbackId, request.getAssetId(), item.getSuggestedLabel());
237
+ break;
238
+ case FLAGGED:
239
+ log.info("[Feedback ID: {}] Asset: {}, Label '{}' flagged for review.", feedbackId, request.getAssetId(), item.getSuggestedLabel());
240
+ break;
241
+ }
242
+ } catch (Exception e) {
243
+ log.error("[Feedback ID: {}] Error processing feedback item for asset {}: {} - {}", feedbackId, request.getAssetId(), item, e.getMessage(), e);
244
+ }
245
+ }
246
+
247
+ feedbackEntity.setProcessingStatus("PROCESSED");
248
+ feedbackEntity.setProcessedAt(LocalDateTime.now());
249
+ labelingFeedbackRepository.save(feedbackEntity);
250
+
251
+ return LabelingFeedbackResponse.builder()
252
+ .feedbackId(feedbackId)
253
+ .assetId(request.getAssetId())
254
+ .status(feedbackEntity.getProcessingStatus())
255
+ .message("Feedback processed successfully.")
256
+ .receivedAt(feedbackEntity.getReceivedAt())
257
+ .build();
258
+ }
259
+
260
+ private List<AssetIdentifier> resolveAssetIdentifiers(LabelingJobRequest.LabelingScope scope) throws AutoLabelingException {
261
+ if (scope == null) return List.of();
262
+
263
+ List<AssetIdentifier> assets = new ArrayList<>();
264
+ if (scope.getAssetIds() != null && !scope.getAssetIds().isEmpty()) {
265
+ assets.addAll(scope.getAssetIds().stream().map(id -> AssetIdentifier.builder().assetId(id).build()).collect(Collectors.toList()));
266
+ }
267
+
268
+ if (assets.isEmpty() && scope.getCloudConnectionId() != null) {
269
+ log.warn("Mocking asset resolution for cloudConnectionId: {}", scope.getCloudConnectionId());
270
+ assets.add(AssetIdentifier.builder().assetId("conn-" + scope.getCloudConnectionId() + "-asset1").provider("MOCK_CLOUD").build());
271
+ assets.add(AssetIdentifier.builder().assetId("conn-" + scope.getCloudConnectionId() + "-asset2").provider("MOCK_CLOUD").build());
272
+ }
273
+
274
+ if (assets.isEmpty()){
275
+ log.warn("No asset resolution strategy matched for scope, returning generic mock assets.");
276
+ return List.of(AssetIdentifier.builder().assetId("mock-global-asset-001").build(), AssetIdentifier.builder().assetId("mock-global-asset-002").build());
277
+ }
278
+ return assets.stream().distinct().collect(Collectors.toList());
279
+ }
280
+ }
src/main/resources/application.properties ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =========================================
2
+ # DALab AutoLabel Service Configuration
3
+ # =========================================
4
+
5
+ # Application
6
+ spring.application.name=da-autolabel
7
+ spring.application.description=DALab Smart Labeling & ML Microservice
8
+ spring.application.version=1.0.0
9
+
10
+ # Server Configuration
11
+ server.port=8080
12
+ server.servlet.context-path=/api/v1/autolabel
13
+ management.server.port=8380
14
+
15
+ # Database Configuration
16
+ # Primary database for da-autolabel service
17
+ spring.datasource.url=jdbc:postgresql://localhost:5432/da_autolabel
18
+ spring.datasource.username=da_autolabel_user
19
+ spring.datasource.password=da_autolabel_pass
20
+ spring.datasource.driver-class-name=org.postgresql.Driver
21
+
22
+ # Additional database for common entities (read-only)
23
+ spring.datasource.common.url=jdbc:postgresql://localhost:5432/da_protos
24
+ spring.datasource.common.username=da_common_user
25
+ spring.datasource.common.password=da_common_pass
26
+ spring.datasource.common.driver-class-name=org.postgresql.Driver
27
+
28
+ # JPA Configuration
29
+ spring.jpa.hibernate.ddl-auto=update
30
+ spring.jpa.show-sql=false
31
+ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
32
+ spring.jpa.properties.hibernate.format_sql=true
33
+ spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
34
+
35
+ # Kafka Configuration
36
+ spring.kafka.bootstrap-servers=localhost:9092
37
+ spring.kafka.consumer.group-id=da-autolabel-consumer-group
38
+ spring.kafka.consumer.auto-offset-reset=earliest
39
+ spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
40
+ spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
41
+ spring.kafka.consumer.properties.spring.json.trusted.packages=*
42
+ spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
43
+ spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
44
+
45
+ # Kafka Topics
46
+ app.kafka.topic.asset-change-event=dalab.assets.changes
47
+ app.kafka.topic.labeling-result=dalab.labeling.results
48
+
49
+ # Security Configuration - Keycloak JWT
50
+ spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/dalab
51
+ spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8180/realms/dalab/protocol/openid-connect/certs
52
+
53
+ # LLM Configuration (placeholders - update with real credentials)
54
+ llm.openai.api-key=${OPENAI_API_KEY:sk-your-openai-api-key}
55
+ llm.openai.model=${OPENAI_MODEL:gpt-3.5-turbo}
56
+ llm.openai.max-tokens=${OPENAI_MAX_TOKENS:1000}
57
+ llm.openai.temperature=${OPENAI_TEMPERATURE:0.3}
58
+
59
+ # Ollama Configuration (for local LLMs)
60
+ llm.ollama.base-url=${OLLAMA_BASE_URL:http://localhost:11434}
61
+ llm.ollama.model=${OLLAMA_MODEL:llama2}
62
+ llm.ollama.timeout=${OLLAMA_TIMEOUT:30000}
63
+
64
+ # Google Gemini Configuration (placeholder)
65
+ llm.gemini.api-key=${GEMINI_API_KEY:your-gemini-api-key}
66
+ llm.gemini.project-id=${GEMINI_PROJECT_ID:your-gcp-project-id}
67
+ llm.gemini.location=${GEMINI_LOCATION:us-central1}
68
+
69
+ # Management Endpoints
70
+ management.endpoints.web.exposure.include=health,info,metrics,prometheus
71
+ management.endpoint.health.show-details=always
72
+ management.health.defaults.enabled=true
73
+ management.health.diskspace.enabled=true
74
+
75
+ # Logging
76
+ logging.level.com.dalab.autolabel=DEBUG
77
+ logging.level.org.springframework.kafka=WARN
78
+ logging.level.org.springframework.security=WARN
79
+ logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
80
+ logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
81
+
82
+ # OpenAPI Documentation
83
+ springdoc.api-docs.path=/v3/api-docs
84
+ springdoc.swagger-ui.path=/swagger-ui.html
85
+ springdoc.swagger-ui.enabled=true
src/test/java/com/dalab/autolabel/controller/LabelingConfigControllerTest.java ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.controller;
2
+
3
+ import static org.mockito.ArgumentMatchers.*;
4
+ import static org.mockito.Mockito.*;
5
+ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
6
+ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
7
+ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
8
+
9
+ import org.junit.jupiter.api.BeforeEach;
10
+ import org.junit.jupiter.api.Test;
11
+ import org.springframework.beans.factory.annotation.Autowired;
12
+ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
13
+ import org.springframework.boot.test.mock.mockito.MockBean;
14
+ import org.springframework.http.MediaType;
15
+ import org.springframework.security.test.context.support.WithMockUser;
16
+ import org.springframework.test.web.servlet.MockMvc;
17
+
18
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
19
+ import com.dalab.autolabel.service.IMLConfigService;
20
+ import com.fasterxml.jackson.databind.ObjectMapper;
21
+
22
+ @WebMvcTest(LabelingConfigController.class)
23
+ class LabelingConfigControllerTest {
24
+
25
+ @Autowired
26
+ private MockMvc mockMvc;
27
+
28
+ @MockBean
29
+ private IMLConfigService mlConfigService;
30
+
31
+ @Autowired
32
+ private ObjectMapper objectMapper;
33
+
34
+ private MLConfigRequest mlConfigRequest;
35
+
36
+ @BeforeEach
37
+ void setUp() {
38
+ mlConfigRequest = MLConfigRequest.builder()
39
+ .providerType("TEST_PROVIDER")
40
+ .modelName("test-model")
41
+ .baseUrl("http://localhost/test")
42
+ .apiKey("test-key")
43
+ .build();
44
+ }
45
+
46
+ @Test
47
+ @WithMockUser(authorities = "ROLE_ADMIN")
48
+ void updateMlConfiguration_AdminRole_ShouldSucceed() throws Exception {
49
+ doNothing().when(mlConfigService).updateMlConfig(any(MLConfigRequest.class));
50
+
51
+ mockMvc.perform(put("/api/v1/labeling/config/ml")
52
+ .with(csrf()) // Add CSRF token for PUT requests if CSRF is enabled
53
+ .contentType(MediaType.APPLICATION_JSON)
54
+ .content(objectMapper.writeValueAsString(mlConfigRequest)))
55
+ .andExpect(status().isOk());
56
+ }
57
+
58
+ @Test
59
+ @WithMockUser(authorities = "ROLE_USER") // Non-admin role
60
+ void updateMlConfiguration_UserRole_ShouldBeForbidden() throws Exception {
61
+ mockMvc.perform(put("/api/v1/labeling/config/ml")
62
+ .with(csrf())
63
+ .contentType(MediaType.APPLICATION_JSON)
64
+ .content(objectMapper.writeValueAsString(mlConfigRequest)))
65
+ .andExpect(status().isForbidden());
66
+ }
67
+
68
+ @Test
69
+ @WithMockUser(authorities = "ROLE_ADMIN")
70
+ void getMlConfiguration_AdminRole_ShouldReturnConfig() throws Exception {
71
+ when(mlConfigService.getMlConfig()).thenReturn(mlConfigRequest);
72
+
73
+ mockMvc.perform(get("/api/v1/labeling/config/ml"))
74
+ .andExpect(status().isOk())
75
+ .andExpect(jsonPath("$.providerType").value("TEST_PROVIDER"))
76
+ .andExpect(jsonPath("$.modelName").value("test-model"));
77
+ }
78
+
79
+ @Test
80
+ @WithMockUser(authorities = "ROLE_ADMIN")
81
+ void getMlConfiguration_AdminRole_NoConfigFound_ShouldReturnNotFound() throws Exception {
82
+ when(mlConfigService.getMlConfig()).thenReturn(null);
83
+
84
+ mockMvc.perform(get("/api/v1/labeling/config/ml"))
85
+ .andExpect(status().isNotFound());
86
+ }
87
+
88
+ @Test
89
+ @WithMockUser(authorities = "ROLE_USER") // Non-admin role
90
+ void getMlConfiguration_UserRole_ShouldBeForbidden() throws Exception {
91
+ mockMvc.perform(get("/api/v1/labeling/config/ml"))
92
+ .andExpect(status().isForbidden());
93
+ }
94
+ }
src/test/java/com/dalab/autolabel/controller/LabelingJobControllerTest.java ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.controller;
2
+
3
+ import com.dalab.autolabel.client.rest.dto.*;
4
+ import com.dalab.autolabel.exception.AutoLabelingException;
5
+ import com.dalab.autolabel.service.ILabelingJobService;
6
+ import com.fasterxml.jackson.databind.ObjectMapper;
7
+ import org.junit.jupiter.api.BeforeEach;
8
+ import org.junit.jupiter.api.Test;
9
+ import org.springframework.beans.factory.annotation.Autowired;
10
+ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
11
+ import org.springframework.boot.test.mock.mockito.MockBean;
12
+ import org.springframework.data.domain.PageImpl;
13
+ import org.springframework.data.domain.PageRequest;
14
+ import org.springframework.data.domain.Pageable;
15
+ import org.springframework.http.MediaType;
16
+ import org.springframework.security.test.context.support.WithMockUser;
17
+ import org.springframework.test.web.servlet.MockMvc;
18
+
19
+ import java.time.LocalDateTime;
20
+ import java.util.Collections;
21
+ import java.util.List;
22
+ import java.util.UUID;
23
+
24
+ import static org.mockito.ArgumentMatchers.any;
25
+ import static org.mockito.ArgumentMatchers.eq;
26
+ import static org.mockito.Mockito.when;
27
+ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
28
+ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
29
+ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
30
+ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
31
+ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
32
+
33
+ @WebMvcTest(LabelingJobController.class)
34
+ class LabelingJobControllerTest {
35
+
36
+ @Autowired
37
+ private MockMvc mockMvc;
38
+
39
+ @MockBean
40
+ private ILabelingJobService labelingJobService;
41
+
42
+ @Autowired
43
+ private ObjectMapper objectMapper;
44
+
45
+ private LabelingJobRequest labelingJobRequest;
46
+ private LabelingJobResponse labelingJobResponse;
47
+ private LabelingJobStatusResponse labelingJobStatusResponse;
48
+ private LabelingFeedbackRequest labelingFeedbackRequest;
49
+ private LabelingFeedbackResponse labelingFeedbackResponse;
50
+
51
+ @BeforeEach
52
+ void setUp() {
53
+ String jobId = UUID.randomUUID().toString();
54
+ labelingJobRequest = LabelingJobRequest.builder().jobName("Test Job").build();
55
+ labelingJobResponse = LabelingJobResponse.builder()
56
+ .jobId(jobId)
57
+ .status("SUBMITTED")
58
+ .submittedAt(LocalDateTime.now())
59
+ .message("Job submitted")
60
+ .build();
61
+ labelingJobStatusResponse = LabelingJobStatusResponse.builder()
62
+ .jobId(jobId).jobName("Test Job").status("RUNNING").build();
63
+
64
+ labelingFeedbackRequest = LabelingFeedbackRequest.builder()
65
+ .assetId("asset-123")
66
+ .labelingJobId(jobId)
67
+ .feedbackItems(List.of(LabelingFeedbackRequest.FeedbackItem.builder()
68
+ .suggestedLabel("PII")
69
+ .type(LabelingFeedbackRequest.FeedbackType.CONFIRMED)
70
+ .build()))
71
+ .build();
72
+ labelingFeedbackResponse = LabelingFeedbackResponse.builder()
73
+ .feedbackId(UUID.randomUUID().toString())
74
+ .assetId("asset-123")
75
+ .status("RECEIVED")
76
+ .build();
77
+ }
78
+
79
+ @Test
80
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
81
+ void submitLabelingJob_ValidRequest_ShouldSucceed() throws Exception {
82
+ when(labelingJobService.submitLabelingJob(any(LabelingJobRequest.class))).thenReturn(labelingJobResponse);
83
+
84
+ mockMvc.perform(post("/api/v1/labeling/jobs")
85
+ .with(csrf())
86
+ .contentType(MediaType.APPLICATION_JSON)
87
+ .content(objectMapper.writeValueAsString(labelingJobRequest)))
88
+ .andExpect(status().isAccepted())
89
+ .andExpect(jsonPath("$.jobId").value(labelingJobResponse.getJobId()));
90
+ }
91
+
92
+ @Test
93
+ @WithMockUser(authorities = "ROLE_USER") // Insufficient role
94
+ void submitLabelingJob_UserRole_ShouldBeForbidden() throws Exception {
95
+ mockMvc.perform(post("/api/v1/labeling/jobs")
96
+ .with(csrf())
97
+ .contentType(MediaType.APPLICATION_JSON)
98
+ .content(objectMapper.writeValueAsString(labelingJobRequest)))
99
+ .andExpect(status().isForbidden());
100
+ }
101
+
102
+ @Test
103
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
104
+ void getJobStatus_ExistingJob_ShouldReturnStatus() throws Exception {
105
+ when(labelingJobService.getJobStatus(eq(labelingJobStatusResponse.getJobId()))).thenReturn(labelingJobStatusResponse);
106
+
107
+ mockMvc.perform(get("/api/v1/labeling/jobs/{jobId}", labelingJobStatusResponse.getJobId()))
108
+ .andExpect(status().isOk())
109
+ .andExpect(jsonPath("$.jobId").value(labelingJobStatusResponse.getJobId()))
110
+ .andExpect(jsonPath("$.status").value("RUNNING"));
111
+ }
112
+
113
+ @Test
114
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
115
+ void getJobStatus_NonExistingJob_ShouldReturnNotFound() throws Exception {
116
+ String nonExistentJobId = UUID.randomUUID().toString();
117
+ when(labelingJobService.getJobStatus(eq(nonExistentJobId))).thenReturn(null);
118
+
119
+ mockMvc.perform(get("/api/v1/labeling/jobs/{jobId}", nonExistentJobId))
120
+ .andExpect(status().isNotFound());
121
+ }
122
+
123
+ @Test
124
+ @WithMockUser(authorities = "ROLE_USER")
125
+ void listJobs_ShouldReturnJobList() throws Exception {
126
+ LabelingJobListResponse listResponse = LabelingJobListResponse.builder()
127
+ .jobs(Collections.singletonList(labelingJobStatusResponse))
128
+ .pageNumber(0).pageSize(20).totalElements(1L).totalPages(1).last(true)
129
+ .build();
130
+ when(labelingJobService.listJobs(any(Pageable.class))).thenReturn(listResponse);
131
+
132
+ mockMvc.perform(get("/api/v1/labeling/jobs").param("page", "0").param("size", "20"))
133
+ .andExpect(status().isOk())
134
+ .andExpect(jsonPath("$.jobs[0].jobId").value(labelingJobStatusResponse.getJobId()));
135
+ }
136
+
137
+ @Test
138
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
139
+ void submitLabelingFeedback_ValidRequest_ShouldSucceed() throws Exception {
140
+ when(labelingJobService.processLabelingFeedback(any(LabelingFeedbackRequest.class))).thenReturn(labelingFeedbackResponse);
141
+
142
+ mockMvc.perform(post("/api/v1/labeling/feedback")
143
+ .with(csrf())
144
+ .contentType(MediaType.APPLICATION_JSON)
145
+ .content(objectMapper.writeValueAsString(labelingFeedbackRequest)))
146
+ .andExpect(status().isOk())
147
+ .andExpect(jsonPath("$.feedbackId").value(labelingFeedbackResponse.getFeedbackId()));
148
+ }
149
+
150
+ @Test
151
+ @WithMockUser(authorities = "ROLE_USER") // Insufficient role
152
+ void submitLabelingFeedback_UserRole_ShouldBeForbidden() throws Exception {
153
+ mockMvc.perform(post("/api/v1/labeling/feedback")
154
+ .with(csrf())
155
+ .contentType(MediaType.APPLICATION_JSON)
156
+ .content(objectMapper.writeValueAsString(labelingFeedbackRequest)))
157
+ .andExpect(status().isForbidden());
158
+ }
159
+
160
+ @Test
161
+ @WithMockUser(authorities = "ROLE_DATA_STEWARD")
162
+ void submitLabelingJob_ServiceThrowsAutoLabelingException_ShouldReturnBadRequest() throws Exception {
163
+ when(labelingJobService.submitLabelingJob(any(LabelingJobRequest.class)))
164
+ .thenThrow(new AutoLabelingException("Test ML config error"));
165
+
166
+ mockMvc.perform(post("/api/v1/labeling/jobs")
167
+ .with(csrf())
168
+ .contentType(MediaType.APPLICATION_JSON)
169
+ .content(objectMapper.writeValueAsString(labelingJobRequest)))
170
+ .andExpect(status().isBadRequest())
171
+ .andExpect(jsonPath("$.message").value("Test ML config error"));
172
+ }
173
+ }
src/test/java/com/dalab/autolabel/service/impl/InMemoryMLConfigServiceTest.java ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service.impl;
2
+
3
+ import static org.junit.jupiter.api.Assertions.*;
4
+
5
+ import org.junit.jupiter.api.BeforeEach;
6
+ import org.junit.jupiter.api.Test;
7
+
8
+ import com.dalab.autolabel.client.rest.dto.MLConfigRequest;
9
+
10
+ class InMemoryMLConfigServiceTest {
11
+
12
+ private InMemoryMLConfigService mlConfigService;
13
+
14
+ @BeforeEach
15
+ void setUp() {
16
+ mlConfigService = new InMemoryMLConfigService();
17
+ }
18
+
19
+ @Test
20
+ void getMlConfig_Initial_ShouldBeNull() {
21
+ assertNull(mlConfigService.getMlConfig(), "Initial config should be null");
22
+ }
23
+
24
+ @Test
25
+ void updateMlConfig_AndGetMlConfig_ShouldReturnUpdatedConfig() {
26
+ MLConfigRequest newConfig = MLConfigRequest.builder()
27
+ .providerType("OPENAI")
28
+ .modelName("gpt-4")
29
+ .apiKey("sk-123")
30
+ .build();
31
+
32
+ mlConfigService.updateMlConfig(newConfig);
33
+ MLConfigRequest fetchedConfig = mlConfigService.getMlConfig();
34
+
35
+ assertNotNull(fetchedConfig, "Fetched config should not be null after update");
36
+ assertEquals("OPENAI", fetchedConfig.getProviderType());
37
+ assertEquals("gpt-4", fetchedConfig.getModelName());
38
+ assertEquals("sk-123", fetchedConfig.getApiKey());
39
+ }
40
+
41
+ @Test
42
+ void updateMlConfig_WithNull_ShouldThrowIllegalArgumentException() {
43
+ assertThrows(IllegalArgumentException.class, () -> {
44
+ mlConfigService.updateMlConfig(null);
45
+ }, "Updating with null config should throw IllegalArgumentException");
46
+ }
47
+
48
+ @Test
49
+ void updateMlConfig_MultipleUpdates_ShouldReflectLatest() {
50
+ MLConfigRequest config1 = MLConfigRequest.builder().modelName("model1").build();
51
+ MLConfigRequest config2 = MLConfigRequest.builder().modelName("model2").build();
52
+
53
+ mlConfigService.updateMlConfig(config1);
54
+ assertEquals("model1", mlConfigService.getMlConfig().getModelName());
55
+
56
+ mlConfigService.updateMlConfig(config2);
57
+ assertEquals("model2", mlConfigService.getMlConfig().getModelName());
58
+ }
59
+ }
src/test/java/com/dalab/autolabel/service/impl/LabelingJobServiceImplTest.java ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.autolabel.service.impl;
2
+
3
+ import com.dalab.autolabel.client.dto.AssetIdentifier;
4
+ import com.dalab.autolabel.client.feign.AssetCatalogClient;
5
+ import com.dalab.autolabel.client.rest.dto.*;
6
+ import com.dalab.autolabel.entity.LabelingFeedbackEntity;
7
+ import com.dalab.autolabel.entity.LabelingJobEntity;
8
+ import com.dalab.autolabel.exception.AutoLabelingException;
9
+ import com.dalab.autolabel.mapper.LabelingJobMapper;
10
+ import com.dalab.autolabel.repository.LabelingFeedbackRepository;
11
+ import com.dalab.autolabel.repository.LabelingJobRepository;
12
+ import com.dalab.autolabel.service.IMLConfigService;
13
+ import com.dalab.autolabel.service.ILLMIntegrationService;
14
+ import org.junit.jupiter.api.BeforeEach;
15
+ import org.junit.jupiter.api.Test;
16
+ import org.junit.jupiter.api.extension.ExtendWith;
17
+ import org.mockito.ArgumentCaptor;
18
+ import org.mockito.InjectMocks;
19
+ import org.mockito.Mock;
20
+ import org.mockito.junit.jupiter.MockitoExtension;
21
+ import org.springframework.data.domain.Page;
22
+ import org.springframework.data.domain.PageImpl;
23
+ import org.springframework.data.domain.PageRequest;
24
+ import org.springframework.data.domain.Pageable;
25
+
26
+ import java.time.LocalDateTime;
27
+ import java.util.Collections;
28
+ import java.util.List;
29
+ import java.util.Optional;
30
+ import java.util.UUID;
31
+
32
+ import static org.junit.jupiter.api.Assertions.*;
33
+ import static org.mockito.ArgumentMatchers.any;
34
+ import static org.mockito.ArgumentMatchers.anyString;
35
+ import static org.mockito.Mockito.*;
36
+
37
+ @ExtendWith(MockitoExtension.class)
38
+ class LabelingJobServiceImplTest {
39
+
40
+ @Mock
41
+ private IMLConfigService mlConfigService;
42
+ @Mock
43
+ private LabelingJobRepository labelingJobRepository;
44
+ @Mock
45
+ private LabelingFeedbackRepository labelingFeedbackRepository;
46
+ @Mock
47
+ private LabelingJobMapper labelingJobMapper;
48
+ @Mock
49
+ private AssetCatalogClient assetCatalogClient; // Mocked, not used in these specific tests yet
50
+ @Mock
51
+ private ILLMIntegrationService llmIntegrationService; // Mocked for async part
52
+
53
+ @InjectMocks
54
+ private LabelingJobServiceImpl labelingJobService;
55
+
56
+ private MLConfigRequest mlConfigRequest;
57
+ private LabelingJobRequest labelingJobRequest;
58
+ private LabelingJobEntity labelingJobEntity;
59
+ private LabelingJobStatusResponse labelingJobStatusResponse;
60
+
61
+ @BeforeEach
62
+ void setUp() {
63
+ mlConfigRequest = MLConfigRequest.builder().providerType("MOCK").modelName("mock-model").build();
64
+ labelingJobRequest = LabelingJobRequest.builder()
65
+ .jobName("Test Job")
66
+ .scope(LabelingJobRequest.LabelingScope.builder().assetIds(List.of("asset1")).build())
67
+ .build();
68
+
69
+ String jobId = UUID.randomUUID().toString();
70
+ labelingJobEntity = LabelingJobEntity.builder()
71
+ .jobId(jobId)
72
+ .jobName("Test Job")
73
+ .status(LabelingJobEntity.JobStatus.SUBMITTED)
74
+ .submittedAt(LocalDateTime.now())
75
+ .build();
76
+ labelingJobStatusResponse = LabelingJobStatusResponse.builder()
77
+ .jobId(jobId)
78
+ .jobName("Test Job")
79
+ .status("SUBMITTED")
80
+ .build();
81
+ }
82
+
83
+ @Test
84
+ void submitLabelingJob_ValidRequest_ShouldSaveJobAndReturnResponse() throws AutoLabelingException {
85
+ when(mlConfigService.getMlConfig()).thenReturn(mlConfigRequest);
86
+ when(labelingJobRepository.save(any(LabelingJobEntity.class))).thenReturn(labelingJobEntity);
87
+ // Mocking async method to avoid its execution. We are testing the synchronous part here.
88
+ // A more robust way might involve a TestExecutionListener or checking side effects.
89
+ LabelingJobServiceImpl spyService = spy(labelingJobService);
90
+ doNothing().when(spyService).processLabelingJobAsync(anyString(), anyString(), any(LabelingJobRequest.LabelingScope.class), any(MLConfigRequest.class));
91
+
92
+ LabelingJobResponse response = spyService.submitLabelingJob(labelingJobRequest);
93
+
94
+ assertNotNull(response);
95
+ assertEquals("SUBMITTED", response.getStatus());
96
+ assertNotNull(response.getJobId());
97
+ verify(labelingJobRepository).save(any(LabelingJobEntity.class));
98
+ verify(spyService).processLabelingJobAsync(eq(response.getJobId()), eq(labelingJobRequest.getJobName()), eq(labelingJobRequest.getScope()), eq(mlConfigRequest));
99
+ }
100
+
101
+ @Test
102
+ void submitLabelingJob_NoMlConfig_ShouldThrowAutoLabelingException() {
103
+ when(mlConfigService.getMlConfig()).thenReturn(null);
104
+ assertThrows(AutoLabelingException.class, () -> {
105
+ labelingJobService.submitLabelingJob(labelingJobRequest);
106
+ });
107
+ verify(labelingJobRepository, never()).save(any(LabelingJobEntity.class));
108
+ }
109
+
110
+ @Test
111
+ void getJobStatus_ExistingJob_ShouldReturnStatusResponse() {
112
+ when(labelingJobRepository.findById(anyString())).thenReturn(Optional.of(labelingJobEntity));
113
+ when(labelingJobMapper.toStatusResponse(any(LabelingJobEntity.class))).thenReturn(labelingJobStatusResponse);
114
+
115
+ LabelingJobStatusResponse response = labelingJobService.getJobStatus("some-job-id");
116
+
117
+ assertNotNull(response);
118
+ assertEquals(labelingJobEntity.getJobId(), response.getJobId());
119
+ verify(labelingJobRepository).findById(eq("some-job-id"));
120
+ }
121
+
122
+ @Test
123
+ void getJobStatus_NonExistingJob_ShouldReturnNull() {
124
+ when(labelingJobRepository.findById(anyString())).thenReturn(Optional.empty());
125
+ LabelingJobStatusResponse response = labelingJobService.getJobStatus("non-existent-id");
126
+ assertNull(response);
127
+ verify(labelingJobMapper, never()).toStatusResponse(any());
128
+ }
129
+
130
+ @Test
131
+ void listJobs_ShouldReturnPaginatedResponse() {
132
+ Pageable pageable = PageRequest.of(0, 10);
133
+ Page<LabelingJobEntity> page = new PageImpl<>(Collections.singletonList(labelingJobEntity), pageable, 1);
134
+ LabelingJobListResponse expectedResponse = LabelingJobListResponse.builder().jobs(List.of(labelingJobStatusResponse)).build();
135
+ // Mapper is complex, so we trust its unit tests and mock its output directly for listJobs
136
+
137
+ when(labelingJobRepository.findAll(any(Pageable.class))).thenReturn(page);
138
+ when(labelingJobMapper.toJobListResponse(page)).thenReturn(expectedResponse);
139
+
140
+ LabelingJobListResponse actualResponse = labelingJobService.listJobs(pageable);
141
+
142
+ assertNotNull(actualResponse);
143
+ assertEquals(expectedResponse.getJobs().size(), actualResponse.getJobs().size());
144
+ verify(labelingJobRepository).findAll(pageable);
145
+ verify(labelingJobMapper).toJobListResponse(page);
146
+ }
147
+
148
+ @Test
149
+ void processLabelingFeedback_ValidRequest_ShouldSaveFeedback() throws AutoLabelingException {
150
+ LabelingFeedbackRequest feedbackRequest = LabelingFeedbackRequest.builder()
151
+ .assetId("asset-1")
152
+ .labelingJobId("job-1")
153
+ .feedbackItems(List.of(LabelingFeedbackRequest.FeedbackItem.builder().suggestedLabel("Old").correctedLabel("New").type(LabelingFeedbackRequest.FeedbackType.CORRECTED).build()))
154
+ .build();
155
+
156
+ ArgumentCaptor<LabelingFeedbackEntity> feedbackEntityCaptor = ArgumentCaptor.forClass(LabelingFeedbackEntity.class);
157
+ when(labelingFeedbackRepository.save(any(LabelingFeedbackEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
158
+
159
+ LabelingFeedbackResponse response = labelingJobService.processLabelingFeedback(feedbackRequest);
160
+
161
+ assertNotNull(response);
162
+ assertEquals("PROCESSED", response.getStatus());
163
+ assertNotNull(response.getFeedbackId());
164
+ verify(labelingFeedbackRepository, times(2)).save(feedbackEntityCaptor.capture()); // Saved once for RECEIVED, once for PROCESSED
165
+
166
+ LabelingFeedbackEntity savedEntity = feedbackEntityCaptor.getValue();
167
+ assertEquals(feedbackRequest.getAssetId(), savedEntity.getAssetId());
168
+ assertEquals("PROCESSED", savedEntity.getProcessingStatus());
169
+ }
170
+
171
+ @Test
172
+ void processLabelingFeedback_EmptyItems_ShouldThrowException() {
173
+ LabelingFeedbackRequest feedbackRequest = LabelingFeedbackRequest.builder().feedbackItems(Collections.emptyList()).build();
174
+ assertThrows(AutoLabelingException.class, () -> {
175
+ labelingJobService.processLabelingFeedback(feedbackRequest);
176
+ });
177
+ verify(labelingFeedbackRepository, never()).save(any());
178
+ }
179
+ }