Spaces:
Build error
Build error
Ajay Yadav
commited on
Commit
·
86a3cc6
1
Parent(s):
5311988
Initial deployment of da-autolabel-dev
Browse files- Dockerfile +27 -0
- README.md +34 -6
- build.gradle.kts +23 -0
- src/main/docker/Dockerfile +23 -0
- src/main/docker/Dockerfile.alpine-jlink +43 -0
- src/main/docker/Dockerfile.layered +34 -0
- src/main/docker/Dockerfile.native +20 -0
- src/main/java/com/dalab/autolabel/DaAutolabelApplication.java +34 -0
- src/main/java/com/dalab/autolabel/client/dto/AssetIdentifier.java +12 -0
- src/main/java/com/dalab/autolabel/client/feign/AssetCatalogClient.java +39 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/LabelingFeedbackRequest.java +64 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/LabelingFeedbackResponse.java +30 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobListResponse.java +47 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobRequest.java +82 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobResponse.java +83 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/LabelingJobStatusResponse.java +49 -0
- src/main/java/com/dalab/autolabel/client/rest/dto/MLConfigRequest.java +108 -0
- src/main/java/com/dalab/autolabel/controller/AutoLabelingController.java +239 -0
- src/main/java/com/dalab/autolabel/controller/LabelingConfigController.java +74 -0
- src/main/java/com/dalab/autolabel/controller/LabelingJobController.java +116 -0
- src/main/java/com/dalab/autolabel/entity/LabelingFeedbackEntity.java +52 -0
- src/main/java/com/dalab/autolabel/entity/LabelingJobEntity.java +72 -0
- src/main/java/com/dalab/autolabel/exception/AutoLabelingException.java +17 -0
- src/main/java/com/dalab/autolabel/llm/client/ILLMClient.java +75 -0
- src/main/java/com/dalab/autolabel/llm/client/LLMClientFactory.java +137 -0
- src/main/java/com/dalab/autolabel/llm/client/impl/MockLLMClient.java +138 -0
- src/main/java/com/dalab/autolabel/llm/client/impl/OllamaLLMClient.java +355 -0
- src/main/java/com/dalab/autolabel/llm/client/impl/OpenAiLLMClient.java +328 -0
- src/main/java/com/dalab/autolabel/llm/model/LLMResponse.java +80 -0
- src/main/java/com/dalab/autolabel/llm/model/LabelSuggestion.java +36 -0
- src/main/java/com/dalab/autolabel/mapper/LabelingJobMapper.java +38 -0
- src/main/java/com/dalab/autolabel/repository/LabelingFeedbackRepository.java +19 -0
- src/main/java/com/dalab/autolabel/repository/LabelingJobRepository.java +13 -0
- src/main/java/com/dalab/autolabel/security/SecurityUtils.java +1 -0
- src/main/java/com/dalab/autolabel/service/AutoLabelingService.java +395 -0
- src/main/java/com/dalab/autolabel/service/ILLMIntegrationService.java +21 -0
- src/main/java/com/dalab/autolabel/service/ILabelingJobService.java +52 -0
- src/main/java/com/dalab/autolabel/service/IMLConfigService.java +24 -0
- src/main/java/com/dalab/autolabel/service/impl/InMemoryMLConfigService.java +53 -0
- src/main/java/com/dalab/autolabel/service/impl/LLMIntegrationServiceImpl.java +69 -0
- src/main/java/com/dalab/autolabel/service/impl/LabelingJobServiceImpl.java +280 -0
- src/main/resources/application.properties +85 -0
- src/test/java/com/dalab/autolabel/controller/LabelingConfigControllerTest.java +94 -0
- src/test/java/com/dalab/autolabel/controller/LabelingJobControllerTest.java +173 -0
- src/test/java/com/dalab/autolabel/service/impl/InMemoryMLConfigServiceTest.java +59 -0
- 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:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
-
|
8 |
---
|
9 |
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|