initial commit
This commit is contained in:
4
dc-backend/.dockerignore
Normal file
4
dc-backend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
target/
|
||||
.mvn/
|
||||
*.md
|
||||
dev.sh
|
||||
44
dc-backend/Dockerfile
Normal file
44
dc-backend/Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
# =============================================================================
|
||||
# Stage 1: Build
|
||||
# =============================================================================
|
||||
FROM maven:3.9-eclipse-temurin-21 AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Dependencies zuerst cachen (Layer-Cache bei Code-Änderungen stabil)
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -q
|
||||
|
||||
COPY src ./src
|
||||
RUN mvn package -DskipTests -q
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Runtime
|
||||
# =============================================================================
|
||||
FROM eclipse-temurin:21-jre
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Quarkus Fast-JAR Layout kopieren
|
||||
COPY --from=build /build/target/quarkus-app/lib/ ./lib/
|
||||
COPY --from=build /build/target/quarkus-app/*.jar ./
|
||||
COPY --from=build /build/target/quarkus-app/app/ ./app/
|
||||
COPY --from=build /build/target/quarkus-app/quarkus/ ./quarkus/
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
# Alle Konfigurationswerte können als Env-Variablen überschrieben werden:
|
||||
#
|
||||
# DC_API_KEY API-Key für eingehende Anfragen
|
||||
# DC_AI_BASE_URL Ollama-Endpunkt
|
||||
# DC_AI_API_KEY Ollama X-API-KEY
|
||||
# DC_AI_MAIN_MODEL Hauptmodell (Übersetzung + Auswertung)
|
||||
# DC_AI_OCR_MODEL OCR-Modell
|
||||
# QUARKUS_REST_CLIENT_ORDS_API_URL ORDS-Basis-URL
|
||||
|
||||
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD curl -sf http://localhost:8090/check/health || exit 1
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar quarkus-run.jar"]
|
||||
44
dc-backend/build-and-push.sh
Normal file
44
dc-backend/build-and-push.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# Build dc-backend Docker-Image und push auf Registry
|
||||
# =============================================================================
|
||||
|
||||
REGISTRY="registry.express-o.org"
|
||||
IMAGE_NAME="frigosped/dc-backend"
|
||||
TAG="${1:-latest}"
|
||||
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$TAG"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "=== dc-backend Build & Push ==="
|
||||
echo "Image : $FULL_IMAGE"
|
||||
echo ""
|
||||
|
||||
# --- Build ---
|
||||
echo "→ Build Image..."
|
||||
podman build \
|
||||
--format docker \
|
||||
--tag "$FULL_IMAGE" \
|
||||
--label "build.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--label "build.version=$TAG" \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
# Bei explizitem Tag zusätzlich 'latest' setzen
|
||||
if [ "$TAG" != "latest" ]; then
|
||||
podman tag "$FULL_IMAGE" "$REGISTRY/$IMAGE_NAME:latest"
|
||||
echo "→ Auch getaggt als: $REGISTRY/$IMAGE_NAME:latest"
|
||||
fi
|
||||
|
||||
# --- Push ---
|
||||
echo "→ Push $FULL_IMAGE ..."
|
||||
podman push "$FULL_IMAGE"
|
||||
|
||||
if [ "$TAG" != "latest" ]; then
|
||||
echo "→ Push $REGISTRY/$IMAGE_NAME:latest ..."
|
||||
podman push "$REGISTRY/$IMAGE_NAME:latest"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Fertig: $FULL_IMAGE"
|
||||
5
dc-backend/dev.sh
Normal file
5
dc-backend/dev.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
cd "$(dirname "$0")"
|
||||
exec quarkus dev
|
||||
88
dc-backend/k8s/deployment.yaml
Normal file
88
dc-backend/k8s/deployment.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dc-backend
|
||||
namespace: ai-env
|
||||
labels:
|
||||
app: dc-backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dc-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dc-backend
|
||||
spec:
|
||||
containers:
|
||||
- name: dc-backend
|
||||
image: registry.express-o.org/frigosped/dc-backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8090
|
||||
env:
|
||||
# ORDS-Verbindung (Service im selben Namespace)
|
||||
- name: QUARKUS_REST_CLIENT_ORDS_API_URL
|
||||
value: "http://ords:8080/ords/FRIGOSPED_APP"
|
||||
# API-Key für eingehende Anfragen (DC_API_KEY)
|
||||
- name: DC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dc-backend-secrets
|
||||
key: api-key
|
||||
optional: true
|
||||
# Ollama KI-Endpunkt
|
||||
- name: DC_AI_BASE_URL
|
||||
value: "https://ollama.aquantico.de"
|
||||
- name: DC_AI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dc-backend-secrets
|
||||
key: ollama-api-key
|
||||
optional: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /check/health
|
||||
port: 8090
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /check/health
|
||||
port: 8090
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dc-backend
|
||||
namespace: ai-env
|
||||
labels:
|
||||
app: dc-backend
|
||||
spec:
|
||||
selector:
|
||||
app: dc-backend
|
||||
ports:
|
||||
- port: 8090
|
||||
targetPort: 8090
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dc-backend-secrets
|
||||
namespace: ai-env
|
||||
type: Opaque
|
||||
stringData:
|
||||
api-key: ""
|
||||
ollama-api-key: "324GF44-50AA-4B57-9386-K435DLJ764DFR"
|
||||
18
dc-backend/logs.sh
Normal file
18
dc-backend/logs.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
export KUBECONFIG="$SCRIPT_DIR/../env/frigo-dev.yaml"
|
||||
|
||||
POD=$(kubectl get pod -n ai-env -l app=dc-backend \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||
|
||||
if [ -z "$POD" ]; then
|
||||
echo "Kein laufender dc-backend Pod gefunden. Alle Pods:"
|
||||
kubectl get pods -n ai-env -l app=dc-backend
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Logs: $POD ==="
|
||||
kubectl logs -f "$POD" -n ai-env
|
||||
146
dc-backend/pom.xml
Normal file
146
dc-backend/pom.xml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>de.frigosped</groupId>
|
||||
<artifactId>dc-backend</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<quarkus.platform.version>3.15.1</quarkus.platform.version>
|
||||
<langchain4j.version>0.36.2</langchain4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-bom</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- ================================================================ -->
|
||||
<!-- Quarkus REST Server -->
|
||||
<!-- ================================================================ -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MicroProfile REST Client für ORDS-Aufrufe -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-client-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Config -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAPI / Swagger UI (optional, für Entwicklung) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- LangChain4J (roh, ohne Quarkus-Extension – volle Kontrolle -->
|
||||
<!-- über Modell-Konfiguration und Custom-Headers) -->
|
||||
<!-- ================================================================ -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-ollama</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Dokumentenverarbeitung -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<!-- PDF → Seitenbild (für KI-OCR) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DOCX → Markdown -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ODT → Text -->
|
||||
<dependency>
|
||||
<groupId>org.odftoolkit</groupId>
|
||||
<artifactId>odfdom-java</artifactId>
|
||||
<version>0.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Tests -->
|
||||
<!-- ================================================================ -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
110
dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
Normal file
110
dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package de.frigosped.dc.ai;
|
||||
|
||||
import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
import dev.langchain4j.model.ollama.OllamaChatModel;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Named;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* CDI-Producer für die zwei KI-Modelle.
|
||||
*
|
||||
* Beide Modelle sprechen denselben Ollama-kompatiblen Endpunkt an,
|
||||
* unterscheiden sich aber in Modellname und Timeout.
|
||||
* Der X-API-KEY-Header wird über customHeaders gesetzt.
|
||||
*
|
||||
* Einsatz:
|
||||
* @Inject @Named("main") ChatLanguageModel mainModel → Auswertung + Übersetzung
|
||||
* @Inject @Named("ocr") ChatLanguageModel ocrModel → PDF-OCR (Vision)
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AiModelProducer {
|
||||
|
||||
@ConfigProperty(name = "dc.ai.base-url")
|
||||
String baseUrl;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.api-key")
|
||||
String apiKey;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.model")
|
||||
String mainModelName;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.timeout", defaultValue = "300s")
|
||||
String mainTimeout;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.max-retries", defaultValue = "2")
|
||||
int mainMaxRetries;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.main.temperature", defaultValue = "0.1")
|
||||
double mainTemperature;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.ocr.model")
|
||||
String ocrModelName;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.ocr.timeout", defaultValue = "120s")
|
||||
String ocrTimeout;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.ocr.max-retries", defaultValue = "2")
|
||||
int ocrMaxRetries;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.log-requests", defaultValue = "false")
|
||||
boolean logRequests;
|
||||
|
||||
@ConfigProperty(name = "dc.ai.log-responses", defaultValue = "false")
|
||||
boolean logResponses;
|
||||
|
||||
/**
|
||||
* Hauptmodell: Auswertung der Fragen + Übersetzung
|
||||
* Modell: gpt-oss:20b
|
||||
*/
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
@Named("main")
|
||||
public ChatLanguageModel mainModel() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(mainModelName)
|
||||
.temperature(mainTemperature)
|
||||
.timeout(parseDuration(mainTimeout))
|
||||
.maxRetries(mainMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* OCR-Modell: Bildtext-Extraktion aus PDF-Seiten (Vision-Modus)
|
||||
* Modell: quen3.5:9b
|
||||
*/
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
@Named("ocr")
|
||||
public ChatLanguageModel ocrModel() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(ocrModelName)
|
||||
.timeout(parseDuration(ocrTimeout))
|
||||
.maxRetries(ocrMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst Timeout-Strings wie "300s", "5m", "120s" in Duration.
|
||||
*/
|
||||
private Duration parseDuration(String value) {
|
||||
if (value.endsWith("s")) {
|
||||
return Duration.ofSeconds(Long.parseLong(value.replace("s", "")));
|
||||
} else if (value.endsWith("m")) {
|
||||
return Duration.ofMinutes(Long.parseLong(value.replace("m", "")));
|
||||
}
|
||||
return Duration.ofSeconds(Long.parseLong(value));
|
||||
}
|
||||
}
|
||||
125
dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
Normal file
125
dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package de.frigosped.dc.client;
|
||||
|
||||
import de.frigosped.dc.model.*;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
/**
|
||||
* MicroProfile REST Client für die ORDS /api/dc/-Endpunkte.
|
||||
*
|
||||
* Basis-URL wird in application.properties gesetzt:
|
||||
* quarkus.rest-client.ords-api.url=http://host/ords/SCHEMA
|
||||
*
|
||||
* Optionaler Bearer-Token für Produktion:
|
||||
* dc.ords.bearer-token=eyJ...
|
||||
* → @ClientHeaderParam(name = "Authorization", value = "${dc.ords.bearer-token}")
|
||||
*/
|
||||
@RegisterRestClient(configKey = "ords-api")
|
||||
@Path("/api/dc")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public interface OrdsClient {
|
||||
|
||||
// =========================================================================
|
||||
// Fragenkatalog (Lesend)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* GET /api/dc/catalogs/:catalogId/questions/
|
||||
* Liefert alle Fragen (mit Kategorie-Info) für einen Katalog.
|
||||
*/
|
||||
@GET
|
||||
@Path("/catalogs/{catalogId}/questions/")
|
||||
OrdsQuestionList getQuestions(@PathParam("catalogId") long catalogId);
|
||||
|
||||
// =========================================================================
|
||||
// Projekte (Status-Steuerung)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* GET /api/dc/projects/:projectId
|
||||
* Projektdetails laden (Status, catalog_id, notification_email, ...).
|
||||
*/
|
||||
@GET
|
||||
@Path("/projects/{projectId}")
|
||||
OrdsProject getProject(@PathParam("projectId") long projectId);
|
||||
|
||||
/**
|
||||
* POST /api/dc/projects/:projectId/start
|
||||
* Setzt Status auf IN_PROGRESS. 409 wenn nicht PENDING.
|
||||
*/
|
||||
@POST
|
||||
@Path("/projects/{projectId}/start")
|
||||
Response startProject(@PathParam("projectId") long projectId);
|
||||
|
||||
/**
|
||||
* PUT /api/dc/projects/:projectId/progress
|
||||
* Aktualisiert den Fortschritt (0–100).
|
||||
*/
|
||||
@PUT
|
||||
@Path("/projects/{projectId}/progress")
|
||||
Response updateProgress(@PathParam("projectId") long projectId,
|
||||
ProgressRequest body);
|
||||
|
||||
/**
|
||||
* POST /api/dc/projects/:projectId/complete
|
||||
* Setzt Status auf COMPLETED + completed_at = SYSDATE.
|
||||
*/
|
||||
@POST
|
||||
@Path("/projects/{projectId}/complete")
|
||||
Response completeProject(@PathParam("projectId") long projectId);
|
||||
|
||||
// =========================================================================
|
||||
// Dokumente
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* GET /api/dc/projects/:projectId/documents/
|
||||
* Liste aller Dokumente (Metadaten, kein BLOB).
|
||||
*/
|
||||
@GET
|
||||
@Path("/projects/{projectId}/documents/")
|
||||
OrdsDocumentList getDocuments(@PathParam("projectId") long projectId);
|
||||
|
||||
/**
|
||||
* GET /api/dc/documents/:docId/file
|
||||
* Download des Original-BLOBs.
|
||||
* Content-Type wird von ORDS aus mime_type-Spalte gesetzt.
|
||||
*/
|
||||
@GET
|
||||
@Path("/documents/{docId}/file")
|
||||
@Produces(MediaType.WILDCARD)
|
||||
Response downloadFile(@PathParam("docId") long docId);
|
||||
|
||||
/**
|
||||
* PUT /api/dc/documents/:docId/texts
|
||||
* Schreibt original_text und/oder translated_text zurück.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/documents/{docId}/texts")
|
||||
Response updateTexts(@PathParam("docId") long docId, TextsRequest body);
|
||||
|
||||
// =========================================================================
|
||||
// Ergebnisse
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* POST /api/dc/projects/:projectId/results
|
||||
* Einzelnes Prüfergebnis speichern.
|
||||
*/
|
||||
@POST
|
||||
@Path("/projects/{projectId}/results")
|
||||
Response saveResult(@PathParam("projectId") long projectId,
|
||||
ResultRequest body);
|
||||
|
||||
/**
|
||||
* DELETE /api/dc/projects/:projectId/results
|
||||
* Alle Ergebnisse löschen (vor Re-Processing).
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/projects/{projectId}/results")
|
||||
Response deleteResults(@PathParam("projectId") long projectId);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package de.frigosped.dc.client;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.ws.rs.client.ClientRequestContext;
|
||||
import jakarta.ws.rs.client.ClientRequestFilter;
|
||||
import jakarta.ws.rs.client.ClientResponseFilter;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Loggt jede HTTP-Anfrage und -Antwort des ORDS REST Clients:
|
||||
* - Vollständige URL (Methode + URI)
|
||||
* - Request-Body
|
||||
* - Response-Status + Body
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Javax.ws.rs.Priorities.AUTHENTICATION + 1)
|
||||
public class OrdsLoggingFilter implements ClientRequestFilter, ClientResponseFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(OrdsLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
public void filter(ClientRequestContext ctx) throws IOException {
|
||||
String method = ctx.getMethod();
|
||||
String uri = ctx.getUri().toString();
|
||||
String entity = readRequest(ctx);
|
||||
|
||||
LOG.infof("=== ORDS %s %s ===", method, uri);
|
||||
if (entity != null && !entity.isBlank()) {
|
||||
LOG.infof("REQUEST: %s", entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(ClientRequestContext requestCtx, ClientResponseContext responseCtx) throws IOException {
|
||||
String body = readResponse(responseCtx);
|
||||
LOG.infof("RESPONSE: HTTP %d%s",
|
||||
responseCtx.getStatus(),
|
||||
body != null ? " " + body : "");
|
||||
LOG.infof("=== ORDS %s %s END ===", requestCtx.getMethod(), requestCtx.getUri());
|
||||
}
|
||||
|
||||
private String readRequest(ClientRequestContext ctx) {
|
||||
Object entity = ctx.getEntity();
|
||||
if (entity == null) return null;
|
||||
|
||||
String body;
|
||||
if (entity instanceof byte[] bytes) {
|
||||
body = new String(bytes, StandardCharsets.UTF_8);
|
||||
} else if (entity instanceof String str) {
|
||||
body = str;
|
||||
} else if (entity instanceof List<?> list) {
|
||||
body = list.toString();
|
||||
} else if (entity instanceof Object[] arr) {
|
||||
body = Arrays.toString(arr);
|
||||
} else {
|
||||
body = entity.toString();
|
||||
}
|
||||
if (body.isBlank()) return null;
|
||||
|
||||
// Entity neu setzen — Stream nicht verbrauchen
|
||||
ctx.setEntity(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)),
|
||||
ctx.getHeaders().getFirst("Content-Type", MediaType.APPLICATION_OCTET_STREAM),
|
||||
Long.valueOf(body.length()));
|
||||
return body;
|
||||
}
|
||||
|
||||
private String readResponse(ClientResponseContext ctx) throws IOException {
|
||||
if (ctx.getEntityStream() == null) return null;
|
||||
|
||||
byte[] raw = ctx.getEntityStream().readAllBytes();
|
||||
if (raw.length == 0) return null;
|
||||
|
||||
// Stream neu setzen für nachfolgende Consumer (z.B. Response.readEntity)
|
||||
ctx.setEntityStream(new ByteArrayInputStream(raw));
|
||||
|
||||
if (raw.length > 4000) {
|
||||
return String.format("[%.4k bytes, trunc. zu 4000]", raw.length);
|
||||
}
|
||||
|
||||
String mediaType = ctx.getMediaType() != null ? ctx.getMediaType().toString() : "";
|
||||
if (mediaType.contains("application/json") || mediaType.contains("text/") || mediaType.isEmpty()) {
|
||||
return new String(raw, StandardCharsets.UTF_8);
|
||||
}
|
||||
return String.format("[%d bytes binary]", raw.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Deserialisiertes JSON-Ergebnis aus der KI-Auswertung.
|
||||
* Die KI gibt dieses Objekt als JSON-String zurück.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class EvaluationResult {
|
||||
|
||||
/** Antwort auf Deutsch (Zitate in Originalsprache + Übersetzung) */
|
||||
private String answer;
|
||||
|
||||
/** Erfüllungsgrad 0–100 */
|
||||
private Integer score;
|
||||
|
||||
/** OK | UNKLAR | NOK */
|
||||
private String resultType;
|
||||
|
||||
/** Kommentar auf Deutsch */
|
||||
private String theComment;
|
||||
|
||||
public EvaluationResult() {}
|
||||
|
||||
public String getAnswer() { return answer; }
|
||||
public void setAnswer(String v) { this.answer = v; }
|
||||
|
||||
public Integer getScore() { return score; }
|
||||
public void setScore(Integer v) { this.score = v; }
|
||||
|
||||
public String getResultType() { return resultType; }
|
||||
public void setResultType(String v) { this.resultType = v; }
|
||||
|
||||
public String getTheComment() { return theComment; }
|
||||
public void setTheComment(String v) { this.theComment = v; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO für ORDS-Endpunkt GET /api/dc/projects/:id/documents/
|
||||
* Kein BLOB; has_original_text / has_translated_text kommen als 0/1.
|
||||
*/
|
||||
public class OrdsDocument {
|
||||
|
||||
private Long id;
|
||||
private String filename;
|
||||
private String mimeType;
|
||||
private Long documentTypeId;
|
||||
private Long catalogId;
|
||||
private LocalDateTime uploadedAt;
|
||||
private Integer hasOriginalText; // 0 oder 1
|
||||
private Integer hasTranslatedText; // 0 oder 1
|
||||
private Integer rowVersion;
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime updated;
|
||||
|
||||
public OrdsDocument() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getFilename() { return filename; }
|
||||
public void setFilename(String filename) { this.filename = filename; }
|
||||
|
||||
public String getMimeType() { return mimeType; }
|
||||
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
|
||||
|
||||
public Long getDocumentTypeId() { return documentTypeId; }
|
||||
public void setDocumentTypeId(Long v) { this.documentTypeId = v; }
|
||||
|
||||
public Long getCatalogId() { return catalogId; }
|
||||
public void setCatalogId(Long catalogId) { this.catalogId = catalogId; }
|
||||
|
||||
public LocalDateTime getUploadedAt() { return uploadedAt; }
|
||||
public void setUploadedAt(LocalDateTime v) { this.uploadedAt = v; }
|
||||
|
||||
public Integer getHasOriginalText() { return hasOriginalText; }
|
||||
public void setHasOriginalText(Integer v) { this.hasOriginalText = v; }
|
||||
|
||||
public Integer getHasTranslatedText() { return hasTranslatedText; }
|
||||
public void setHasTranslatedText(Integer v) { this.hasTranslatedText = v; }
|
||||
|
||||
public Integer getRowVersion() { return rowVersion; }
|
||||
public void setRowVersion(Integer v) { this.rowVersion = v; }
|
||||
|
||||
public LocalDateTime getCreated() { return created; }
|
||||
public void setCreated(LocalDateTime v) { this.created = v; }
|
||||
|
||||
public LocalDateTime getUpdated() { return updated; }
|
||||
public void setUpdated(LocalDateTime v) { this.updated = v; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ORDS COLLECTION_FEED-Antwort für Dokumente.
|
||||
* {"items":[...], "hasMore":false, "count":N}
|
||||
*/
|
||||
public class OrdsDocumentList {
|
||||
|
||||
private List<OrdsDocument> items;
|
||||
private Boolean hasMore;
|
||||
private Integer count;
|
||||
|
||||
public OrdsDocumentList() {}
|
||||
|
||||
public List<OrdsDocument> getItems() { return items; }
|
||||
public void setItems(List<OrdsDocument> items) { this.items = items; }
|
||||
|
||||
public Boolean getHasMore() { return hasMore; }
|
||||
public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; }
|
||||
|
||||
public Integer getCount() { return count; }
|
||||
public void setCount(Integer count) { this.count = count; }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO für den ORDS-Endpunkt GET /api/dc/projects/:project_id
|
||||
* (COLLECTION_ITEM – einzelnes JSON-Objekt, snake_case von ORDS)
|
||||
*/
|
||||
public class OrdsProject {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private Long catalogId;
|
||||
private Long createdByUser;
|
||||
private String status; // PENDING | IN_PROGRESS | COMPLETED
|
||||
private Integer progress; // 0–100
|
||||
private LocalDateTime completedAt;
|
||||
private String notificationEmail;
|
||||
private Integer rowVersion;
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime updated;
|
||||
|
||||
public OrdsProject() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String d) { this.description = d; }
|
||||
|
||||
public Long getCatalogId() { return catalogId; }
|
||||
public void setCatalogId(Long catalogId) { this.catalogId = catalogId; }
|
||||
|
||||
public Long getCreatedByUser() { return createdByUser; }
|
||||
public void setCreatedByUser(Long v) { this.createdByUser = v; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public Integer getProgress() { return progress; }
|
||||
public void setProgress(Integer progress) { this.progress = progress; }
|
||||
|
||||
public LocalDateTime getCompletedAt() { return completedAt; }
|
||||
public void setCompletedAt(LocalDateTime v) { this.completedAt = v; }
|
||||
|
||||
public String getNotificationEmail() { return notificationEmail; }
|
||||
public void setNotificationEmail(String v) { this.notificationEmail = v; }
|
||||
|
||||
public Integer getRowVersion() { return rowVersion; }
|
||||
public void setRowVersion(Integer v) { this.rowVersion = v; }
|
||||
|
||||
public LocalDateTime getCreated() { return created; }
|
||||
public void setCreated(LocalDateTime v) { this.created = v; }
|
||||
|
||||
public LocalDateTime getUpdated() { return updated; }
|
||||
public void setUpdated(LocalDateTime v) { this.updated = v; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* DTO für ORDS-Endpunkt GET /api/dc/catalogs/:id/questions/
|
||||
* Flache Darstellung: Kategorie + Frage in einer Zeile.
|
||||
*/
|
||||
public class OrdsQuestion {
|
||||
|
||||
private Long questionId;
|
||||
private Long categoryId;
|
||||
private String categoryName;
|
||||
private String categoryDescription;
|
||||
private String questionText;
|
||||
private String evaluationType; // z.B. SCORE, BINARY
|
||||
private Integer threshold; // Schwellwert 0–100
|
||||
private String resultHandling; // ABWEICHUNG | HINWEIS
|
||||
private String example0Percent; // Beispiel: Anforderung nicht erfüllt
|
||||
private String example100Percent; // Beispiel: Anforderung voll erfüllt
|
||||
private Integer rowVersion;
|
||||
|
||||
public OrdsQuestion() {}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long v) { this.questionId = v; }
|
||||
|
||||
public Long getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(Long v) { this.categoryId = v; }
|
||||
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String v) { this.categoryName = v; }
|
||||
|
||||
public String getCategoryDescription() { return categoryDescription; }
|
||||
public void setCategoryDescription(String v) { this.categoryDescription = v; }
|
||||
|
||||
public String getQuestionText() { return questionText; }
|
||||
public void setQuestionText(String v) { this.questionText = v; }
|
||||
|
||||
public String getEvaluationType() { return evaluationType; }
|
||||
public void setEvaluationType(String v) { this.evaluationType = v; }
|
||||
|
||||
public Integer getThreshold() { return threshold; }
|
||||
public void setThreshold(Integer v) { this.threshold = v; }
|
||||
|
||||
public String getResultHandling() { return resultHandling; }
|
||||
public void setResultHandling(String v) { this.resultHandling = v; }
|
||||
|
||||
public String getExample0Percent() { return example0Percent; }
|
||||
public void setExample0Percent(String v) { this.example0Percent = v; }
|
||||
|
||||
public String getExample100Percent() { return example100Percent; }
|
||||
public void setExample100Percent(String v) { this.example100Percent = v; }
|
||||
|
||||
public Integer getRowVersion() { return rowVersion; }
|
||||
public void setRowVersion(Integer v) { this.rowVersion = v; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ORDS COLLECTION_FEED-Antwort für Fragen.
|
||||
* {"items":[...], "hasMore":false, "count":N}
|
||||
*/
|
||||
public class OrdsQuestionList {
|
||||
|
||||
private List<OrdsQuestion> items;
|
||||
private Boolean hasMore;
|
||||
private Integer count;
|
||||
|
||||
public OrdsQuestionList() {}
|
||||
|
||||
public List<OrdsQuestion> getItems() { return items; }
|
||||
public void setItems(List<OrdsQuestion> items) { this.items = items; }
|
||||
|
||||
public Boolean getHasMore() { return hasMore; }
|
||||
public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; }
|
||||
|
||||
public Integer getCount() { return count; }
|
||||
public void setCount(Integer count) { this.count = count; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* Request-Body für PUT /api/dc/projects/:id/progress
|
||||
* {"progress": 45}
|
||||
*/
|
||||
public class ProgressRequest {
|
||||
|
||||
private Integer progress;
|
||||
|
||||
public ProgressRequest() {}
|
||||
|
||||
public ProgressRequest(int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public Integer getProgress() { return progress; }
|
||||
public void setProgress(Integer v) { this.progress = v; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* Request-Body für POST /api/dc/projects/:id/results
|
||||
*/
|
||||
public class ResultRequest {
|
||||
|
||||
private Long questionId;
|
||||
private Long docId;
|
||||
private String answer;
|
||||
private Integer score;
|
||||
private String resultType; // OK | UNKLAR | NOK
|
||||
private Integer deviation; // 0 oder 1
|
||||
private Integer warning; // 0 oder 1
|
||||
private String theComment;
|
||||
|
||||
public ResultRequest() {}
|
||||
|
||||
public ResultRequest(Long questionId, Long docId, String answer,
|
||||
Integer score, String resultType,
|
||||
Integer deviation, Integer warning, String theComment) {
|
||||
this.questionId = questionId;
|
||||
this.docId = docId;
|
||||
this.answer = answer;
|
||||
this.score = score;
|
||||
this.resultType = resultType;
|
||||
this.deviation = deviation;
|
||||
this.warning = warning;
|
||||
this.theComment = theComment;
|
||||
}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long v) { this.questionId = v; }
|
||||
|
||||
public Long getDocId() { return docId; }
|
||||
public void setDocId(Long v) { this.docId = v; }
|
||||
|
||||
public String getAnswer() { return answer; }
|
||||
public void setAnswer(String v) { this.answer = v; }
|
||||
|
||||
public Integer getScore() { return score; }
|
||||
public void setScore(Integer v) { this.score = v; }
|
||||
|
||||
public String getResultType() { return resultType; }
|
||||
public void setResultType(String v) { this.resultType = v; }
|
||||
|
||||
public Integer getDeviation() { return deviation; }
|
||||
public void setDeviation(Integer v) { this.deviation = v; }
|
||||
|
||||
public Integer getWarning() { return warning; }
|
||||
public void setWarning(Integer v) { this.warning = v; }
|
||||
|
||||
public String getTheComment() { return theComment; }
|
||||
public void setTheComment(String v) { this.theComment = v; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.frigosped.dc.model;
|
||||
|
||||
/**
|
||||
* Request-Body für PUT /api/dc/documents/:id/texts
|
||||
* {"original_text": "...", "translated_text": "..."}
|
||||
*/
|
||||
public class TextsRequest {
|
||||
|
||||
private String originalText;
|
||||
private String translatedText;
|
||||
|
||||
public TextsRequest() {}
|
||||
|
||||
public TextsRequest(String originalText, String translatedText) {
|
||||
this.originalText = originalText;
|
||||
this.translatedText = translatedText;
|
||||
}
|
||||
|
||||
public String getOriginalText() { return originalText; }
|
||||
public void setOriginalText(String v) { this.originalText = v; }
|
||||
|
||||
public String getTranslatedText() { return translatedText; }
|
||||
public void setTranslatedText(String v) { this.translatedText = v; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.frigosped.dc.resource;
|
||||
|
||||
import de.frigosped.dc.service.CheckOrchestrationService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* REST-Endpunkte des Dokumenten-Check-Backend.
|
||||
*
|
||||
* POST /check/{projectId}
|
||||
* → Startet die asynchrone Verarbeitung eines Projekts.
|
||||
* → Gibt sofort 202 Accepted zurück; Fortschritt über ORDS-Endpunkte ablesbar.
|
||||
*
|
||||
* GET /check/health
|
||||
* → Einfacher Health-Check.
|
||||
*/
|
||||
@Path("/check")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Dokumenten-Check", description = "KI-gestützte Dokumentenprüfung")
|
||||
public class CheckResource {
|
||||
|
||||
@Inject
|
||||
CheckOrchestrationService orchestrationService;
|
||||
|
||||
// =========================================================================
|
||||
// POST /check/{projectId}
|
||||
// =========================================================================
|
||||
|
||||
@POST
|
||||
@Path("/{projectId}")
|
||||
@Operation(
|
||||
summary = "Startet die Dokumentenprüfung für ein Projekt",
|
||||
description = """
|
||||
Triggert den vollständigen Prüfablauf asynchron:
|
||||
1. OCR / Konvertierung der Dokumente (KI)
|
||||
2. Übersetzung ins Deutsche (KI)
|
||||
3. Auswertung des Fragenkatalogs (KI)
|
||||
4. Ergebnisse werden in ORDS gespeichert
|
||||
|
||||
Das Projekt muss im Status PENDING sein.
|
||||
Fortschritt: GET {ords}/api/dc/projects/{id}
|
||||
"""
|
||||
)
|
||||
public Response startCheck(@PathParam("projectId") long projectId) {
|
||||
// Asynchron starten – Fire & Forget.
|
||||
// Der aufrufende Thread (z.B. APEX-Button) bekommt sofort 202.
|
||||
// Der Verarbeitungs-Thread läuft im ForkJoin-CommonPool.
|
||||
CompletableFuture
|
||||
.runAsync(() -> orchestrationService.process(projectId))
|
||||
.exceptionally(ex -> {
|
||||
// Der Orchestrator fängt intern; dies ist eine letzte Absicherung.
|
||||
// Logging ist dort bereits vorhanden.
|
||||
return null;
|
||||
});
|
||||
|
||||
return Response
|
||||
.accepted(Map.of(
|
||||
"message", "Verarbeitung gestartet",
|
||||
"project_id", projectId,
|
||||
"hint", "Fortschritt: GET /api/dc/projects/" + projectId
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GET /check/health
|
||||
// =========================================================================
|
||||
|
||||
@GET
|
||||
@Path("/health")
|
||||
@Operation(summary = "Health-Check", description = "Gibt UP zurück wenn der Service läuft.")
|
||||
public Response health() {
|
||||
return Response.ok(Map.of("status", "UP", "service", "dc-backend")).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.frigosped.dc.security;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Prüft den X-API-KEY-Header bei allen Anfragen.
|
||||
* Konfiguration: dc.api.key (Env: DC_API_KEY)
|
||||
* Wenn kein Key konfiguriert ist, läuft der Service ohne Auth (Dev-Modus).
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHENTICATION)
|
||||
public class ApiKeyFilter implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ApiKeyFilter.class);
|
||||
private static final String HEADER = "X-API-KEY";
|
||||
|
||||
@ConfigProperty(name = "dc.api.key")
|
||||
Optional<String> apiKey;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext ctx) {
|
||||
// Health-Endpunkt immer durchlassen (Docker-Healthcheck)
|
||||
if (ctx.getUriInfo().getPath().endsWith("/health")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Kein Key konfiguriert → Auth deaktiviert (Dev-Modus)
|
||||
if (apiKey.isEmpty() || apiKey.get().isBlank()) {
|
||||
LOG.warnf("DC_API_KEY nicht gesetzt – Anfrage ohne Auth durchgelassen: %s",
|
||||
ctx.getUriInfo().getPath());
|
||||
return;
|
||||
}
|
||||
|
||||
String provided = ctx.getHeaderString(HEADER);
|
||||
if (provided == null || !provided.equals(apiKey.get())) {
|
||||
LOG.warnf("Ungültiger oder fehlender API-Key für: %s", ctx.getUriInfo().getPath());
|
||||
ctx.abortWith(Response
|
||||
.status(Response.Status.UNAUTHORIZED)
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(Map.of("error", "Ungültiger oder fehlender API-Key",
|
||||
"header", HEADER))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package de.frigosped.dc.service;
|
||||
|
||||
import de.frigosped.dc.client.OrdsClient;
|
||||
import de.frigosped.dc.model.*;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Orchestriert den kompletten Prüf-Ablauf für ein Projekt:
|
||||
*
|
||||
* 1. Projekt laden → IN_PROGRESS setzen
|
||||
* 2. Dokumente herunterladen → OCR / Konvertierung → Texte speichern
|
||||
* 3. Texte ins Deutsche übersetzen → gespeichert
|
||||
* 4. Fragenkatalog für jeden Dokument-Text auswerten
|
||||
* 5. Ergebnisse Frage × Dokument in DB speichern
|
||||
* 6. Projekt auf COMPLETED setzen
|
||||
*
|
||||
* Wird von CheckResource.startCheck() asynchron (CompletableFuture) aufgerufen.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class CheckOrchestrationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(CheckOrchestrationService.class);
|
||||
|
||||
@RestClient
|
||||
OrdsClient ordsClient;
|
||||
|
||||
@Inject
|
||||
DocumentProcessingService documentProcessingService;
|
||||
|
||||
@Inject
|
||||
EvaluationService evaluationService;
|
||||
|
||||
// =========================================================================
|
||||
// Haupt-Einstiegspunkt
|
||||
// =========================================================================
|
||||
|
||||
public void process(long projectId) {
|
||||
LOG.infof("=== Starte Verarbeitung Projekt %d ===", projectId);
|
||||
|
||||
try {
|
||||
// 1. Projekt laden
|
||||
OrdsProject project = ordsClient.getProject(projectId);
|
||||
LOG.infof("Projekt: '%s', Status: %s, Katalog: %d",
|
||||
project.getName(), project.getStatus(), project.getCatalogId());
|
||||
|
||||
// Absicherung: nur PENDING starten
|
||||
if (!"PENDING".equals(project.getStatus())) {
|
||||
LOG.warnf("Projekt %d ist nicht PENDING (ist: %s) – Abbruch",
|
||||
projectId, project.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Status → IN_PROGRESS
|
||||
Response startResp = ordsClient.startProject(projectId);
|
||||
if (startResp.getStatus() == 409) {
|
||||
LOG.warnf("Projekt %d konnte nicht gestartet werden (409) – Abbruch", projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Dokumente + Fragen laden
|
||||
List<OrdsDocument> documents = ordsClient.getDocuments(projectId).getItems();
|
||||
List<OrdsQuestion> questions = ordsClient
|
||||
.getQuestions(project.getCatalogId()).getItems();
|
||||
|
||||
LOG.infof("Projekt %d: %d Dokument(e), %d Frage(n)",
|
||||
projectId, documents.size(), questions.size());
|
||||
|
||||
if (documents.isEmpty()) {
|
||||
LOG.warnf("Projekt %d hat keine Dokumente – markiere als abgeschlossen", projectId);
|
||||
ordsClient.completeProject(projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Fortschrittsberechnung
|
||||
// Pro Dokument: 1 Schritt OCR/Übersetzung + questions.size() Auswertungsschritte
|
||||
int totalSteps = documents.size() * (1 + questions.size());
|
||||
int[] stepHolder = {0}; // Array für Verwendung in Lambda
|
||||
|
||||
// 5. Alte Ergebnisse löschen (ermöglicht Re-Processing)
|
||||
Response delResp = ordsClient.deleteResults(projectId);
|
||||
LOG.debugf("Alte Ergebnisse gelöscht: %s", delResp.getStatus());
|
||||
|
||||
// 6. Texte sammeln (in-memory: vermeidet Re-Fetch aus ORDS)
|
||||
Map<Long, String> originalTexts = new HashMap<>();
|
||||
|
||||
// ─── PHASE A: OCR + Übersetzung ───────────────────────────────
|
||||
for (OrdsDocument doc : documents) {
|
||||
LOG.infof("Verarbeite Dokument %d: %s", doc.getId(), doc.getFilename());
|
||||
|
||||
String originalText = "";
|
||||
String translatedText = "";
|
||||
|
||||
try {
|
||||
// Datei herunterladen
|
||||
Response fileResp = ordsClient.downloadFile(doc.getId());
|
||||
if (fileResp.getStatus() != 200) {
|
||||
LOG.warnf("Download Dokument %d fehlgeschlagen (HTTP %d)",
|
||||
doc.getId(), fileResp.getStatus());
|
||||
} else {
|
||||
byte[] fileBytes = fileResp.readEntity(byte[].class);
|
||||
|
||||
// OCR / Konvertierung → Markdown
|
||||
originalText = documentProcessingService.extractText(
|
||||
fileBytes, doc.getMimeType(), doc.getFilename());
|
||||
LOG.debugf("Dokument %d: %d Zeichen extrahiert",
|
||||
(long) doc.getId(), (long) originalText.length());
|
||||
|
||||
// Übersetzung ins Deutsche
|
||||
translatedText = evaluationService.translate(originalText);
|
||||
LOG.debugf("Dokument %d: Übersetzung fertig (%d Zeichen)",
|
||||
(long) doc.getId(), (long) translatedText.length());
|
||||
|
||||
// Texte in ORDS zurückschreiben
|
||||
ordsClient.updateTexts(doc.getId(),
|
||||
new TextsRequest(originalText, translatedText));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Fehler bei Dokumentenverarbeitung %d", doc.getId());
|
||||
}
|
||||
|
||||
originalTexts.put(doc.getId(), originalText);
|
||||
|
||||
// Fortschritt aktualisieren
|
||||
stepHolder[0]++;
|
||||
pushProgress(projectId, stepHolder[0], totalSteps);
|
||||
}
|
||||
|
||||
// ─── PHASE B: Fragen auswerten ────────────────────────────────
|
||||
for (OrdsDocument doc : documents) {
|
||||
String originalText = originalTexts.getOrDefault(doc.getId(), "");
|
||||
|
||||
if (originalText.isBlank()) {
|
||||
LOG.warnf("Kein Text für Dokument %d – überspringe Auswertung", doc.getId());
|
||||
stepHolder[0] += questions.size();
|
||||
pushProgress(projectId, stepHolder[0], totalSteps);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (OrdsQuestion question : questions) {
|
||||
try {
|
||||
LOG.debugf("Auswertung: Dok %d × Frage %d",
|
||||
doc.getId(), question.getQuestionId());
|
||||
|
||||
EvaluationResult result = evaluationService.evaluate(question, originalText);
|
||||
|
||||
// Abweichung / Hinweis aus Schwellwert + result_handling berechnen
|
||||
int deviation = 0;
|
||||
int warning = 0;
|
||||
if (result.getScore() != null && question.getThreshold() != null
|
||||
&& result.getScore() < question.getThreshold()) {
|
||||
if ("ABWEICHUNG".equalsIgnoreCase(question.getResultHandling())) {
|
||||
deviation = 1;
|
||||
} else if ("HINWEIS".equalsIgnoreCase(question.getResultHandling())) {
|
||||
warning = 1;
|
||||
}
|
||||
}
|
||||
|
||||
ResultRequest rr = new ResultRequest(
|
||||
question.getQuestionId(),
|
||||
doc.getId(),
|
||||
result.getAnswer(),
|
||||
result.getScore(),
|
||||
result.getResultType(),
|
||||
deviation,
|
||||
warning,
|
||||
result.getTheComment()
|
||||
);
|
||||
|
||||
Response saveResp = ordsClient.saveResult(projectId, rr);
|
||||
if (saveResp.getStatus() != 201) {
|
||||
LOG.warnf("Ergebnis speichern: HTTP %d (Frage %d, Dok %d)",
|
||||
saveResp.getStatus(), question.getQuestionId(), doc.getId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Auswertungsfehler: Frage %d / Dokument %d",
|
||||
question.getQuestionId(), doc.getId());
|
||||
}
|
||||
|
||||
stepHolder[0]++;
|
||||
pushProgress(projectId, stepHolder[0], totalSteps);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Abschließen
|
||||
ordsClient.completeProject(projectId);
|
||||
LOG.infof("=== Projekt %d erfolgreich abgeschlossen ===", projectId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Kritischer Fehler: Projekt bleibt in IN_PROGRESS.
|
||||
// APEX-Oberfläche / Admin muss manuell zurücksetzen.
|
||||
LOG.errorf(e, "=== Kritischer Fehler bei Projekt %d – bleibt IN_PROGRESS ===",
|
||||
projectId);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Sendet den aktuellen Fortschritt an ORDS (0–100%).
|
||||
* Fehler werden nur geloggt, kein Abbruch.
|
||||
*/
|
||||
private void pushProgress(long projectId, int step, int total) {
|
||||
int pct = total > 0 ? Math.min(step * 100 / total, 99) : 0;
|
||||
try {
|
||||
ordsClient.updateProgress(projectId, new ProgressRequest(pct));
|
||||
} catch (Exception e) {
|
||||
LOG.debugf("Fortschritt konnte nicht gesetzt werden (%d%%): %s", pct, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package de.frigosped.dc.service;
|
||||
|
||||
import dev.langchain4j.data.message.ImageContent;
|
||||
import dev.langchain4j.data.message.TextContent;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.poi.xwpf.usermodel.*;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.odftoolkit.odfdom.doc.OdfTextDocument;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Konvertiert hochgeladene Dokumente in Markdown-Text.
|
||||
*
|
||||
* Unterstützte Formate:
|
||||
* - PDF → KI-OCR (jede Seite als Bild an quen3.5:9b)
|
||||
* - DOCX → Apache POI → Markdown
|
||||
* - ODT → ODF Toolkit → Markdown
|
||||
* - TXT → direkt als Markdown
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DocumentProcessingService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DocumentProcessingService.class);
|
||||
|
||||
/** 200 DPI: gutes Gleichgewicht zwischen Qualität und Dateigröße für OCR */
|
||||
private static final float OCR_DPI = 200f;
|
||||
|
||||
@Inject
|
||||
@Named("ocr")
|
||||
ChatLanguageModel ocrModel;
|
||||
|
||||
// =========================================================================
|
||||
// Öffentliche API
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Extrahiert den Textinhalt eines Dokuments als Markdown.
|
||||
*
|
||||
* @param fileBytes rohe Datei-Bytes (aus BLOB)
|
||||
* @param mimeType MIME-Typ des Dokuments
|
||||
* @param filename Dateiname (Fallback für MIME-Typ-Erkennung)
|
||||
* @return Markdown-String
|
||||
*/
|
||||
public String extractText(byte[] fileBytes, String mimeType, String filename) {
|
||||
String effectiveMime = resolveMimeType(mimeType, filename);
|
||||
LOG.debugf("Extrahiere Text: %s (%s)", filename, effectiveMime);
|
||||
|
||||
try {
|
||||
return switch (effectiveMime) {
|
||||
case "application/pdf"
|
||||
-> extractFromPdf(fileBytes);
|
||||
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword"
|
||||
-> extractFromDocx(fileBytes);
|
||||
case "application/vnd.oasis.opendocument.text"
|
||||
-> extractFromOdt(fileBytes);
|
||||
case "text/plain", "text/markdown"
|
||||
-> new String(fileBytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||
default -> {
|
||||
LOG.warnf("Unbekannter MIME-Typ '%s' – versuche als Text", effectiveMime);
|
||||
yield new String(fileBytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Fehler bei Textextraktion (%s)", filename);
|
||||
throw new RuntimeException("Textextraktion fehlgeschlagen: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PDF: Seiten rendern + KI-OCR
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromPdf(byte[] fileBytes) throws Exception {
|
||||
LOG.debug("Starte PDF-OCR...");
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
try (PDDocument pdf = Loader.loadPDF(fileBytes)) {
|
||||
PDFRenderer renderer = new PDFRenderer(pdf);
|
||||
int pageCount = pdf.getNumberOfPages();
|
||||
LOG.debugf("PDF hat %d Seite(n)", pageCount);
|
||||
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
LOG.debugf("OCR Seite %d/%d", i + 1, pageCount);
|
||||
BufferedImage image = renderer.renderImageWithDPI(i, OCR_DPI, ImageType.RGB);
|
||||
|
||||
// Bild in PNG-Base64 umwandeln
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
String base64Image = Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
|
||||
String pageText = ocrPage(base64Image, i + 1, pageCount);
|
||||
result.append(pageText).append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine PDF-Seite als Base64-Bild an das Vision-Modell.
|
||||
*/
|
||||
private String ocrPage(String base64Image, int pageNum, int totalPages) {
|
||||
UserMessage message = UserMessage.from(
|
||||
ImageContent.from(base64Image, "image/png"),
|
||||
TextContent.from(
|
||||
"Du bist ein OCR-Spezialist. " +
|
||||
"Extrahiere den gesamten Text aus diesem Dokument-Bild (Seite " + pageNum
|
||||
+ " von " + totalPages + ").\n\n" +
|
||||
"Regeln:\n" +
|
||||
"- Formatiere den Text als Markdown\n" +
|
||||
"- Überschriften als # / ## / ###\n" +
|
||||
"- Tabellen als Markdown-Tabellen\n" +
|
||||
"- Listen als - oder 1. 2. 3.\n" +
|
||||
"- Behalte den Originaltext exakt (keine Korrekturen, keine Zusammenfassungen)\n" +
|
||||
"- Antworte NUR mit dem extrahierten Markdown, ohne Präambel"
|
||||
)
|
||||
);
|
||||
|
||||
return ocrModel.generate(List.of(message)).content().text();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DOCX → Markdown (Apache POI)
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromDocx(byte[] fileBytes) throws Exception {
|
||||
LOG.debug("Konvertiere DOCX → Markdown...");
|
||||
StringBuilder md = new StringBuilder();
|
||||
|
||||
try (XWPFDocument doc = new XWPFDocument(new ByteArrayInputStream(fileBytes))) {
|
||||
for (IBodyElement element : doc.getBodyElements()) {
|
||||
if (element instanceof XWPFParagraph para) {
|
||||
md.append(paragraphToMarkdown(para)).append("\n");
|
||||
} else if (element instanceof XWPFTable table) {
|
||||
md.append(tableToMarkdown(table)).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return md.toString().trim();
|
||||
}
|
||||
|
||||
private String paragraphToMarkdown(XWPFParagraph para) {
|
||||
String text = para.getText();
|
||||
if (text == null || text.isBlank()) return "";
|
||||
|
||||
String style = para.getStyle();
|
||||
if (style == null) return text;
|
||||
|
||||
// Überschriften
|
||||
if (style.equalsIgnoreCase("Heading1") || style.equalsIgnoreCase("berschrift1"))
|
||||
return "# " + text;
|
||||
if (style.equalsIgnoreCase("Heading2") || style.equalsIgnoreCase("berschrift2"))
|
||||
return "## " + text;
|
||||
if (style.equalsIgnoreCase("Heading3") || style.equalsIgnoreCase("berschrift3"))
|
||||
return "### " + text;
|
||||
if (style.equalsIgnoreCase("Heading4") || style.equalsIgnoreCase("berschrift4"))
|
||||
return "#### " + text;
|
||||
|
||||
// Nummerierte Liste
|
||||
if (para.getNumIlvl() != null) {
|
||||
return "- " + text;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private String tableToMarkdown(XWPFTable table) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<XWPFTableRow> rows = table.getRows();
|
||||
if (rows.isEmpty()) return "";
|
||||
|
||||
// Header-Zeile
|
||||
XWPFTableRow header = rows.get(0);
|
||||
sb.append("| ");
|
||||
header.getTableCells().forEach(c -> sb.append(c.getText()).append(" | "));
|
||||
sb.append("\n|");
|
||||
header.getTableCells().forEach(c -> sb.append("---|"));
|
||||
sb.append("\n");
|
||||
|
||||
// Daten-Zeilen
|
||||
for (int i = 1; i < rows.size(); i++) {
|
||||
sb.append("| ");
|
||||
rows.get(i).getTableCells().forEach(c -> sb.append(c.getText()).append(" | "));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ODT → Markdown (ODF Toolkit)
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromOdt(byte[] fileBytes) throws Exception {
|
||||
LOG.debug("Konvertiere ODT → Markdown...");
|
||||
StringBuilder md = new StringBuilder();
|
||||
|
||||
try (OdfTextDocument odfDoc = OdfTextDocument.loadDocument(
|
||||
new ByteArrayInputStream(fileBytes))) {
|
||||
|
||||
NodeList paragraphs = odfDoc.getContentDom()
|
||||
.getElementsByTagName("text:p");
|
||||
|
||||
for (int i = 0; i < paragraphs.getLength(); i++) {
|
||||
Node node = paragraphs.item(i);
|
||||
String text = node.getTextContent();
|
||||
if (text != null && !text.isBlank()) {
|
||||
// Stil-Attribut auslesen
|
||||
Node styleAttr = node.getAttributes()
|
||||
.getNamedItem("text:style-name");
|
||||
String style = styleAttr != null ? styleAttr.getNodeValue() : "";
|
||||
|
||||
if (style.startsWith("Heading_20_1") || style.startsWith("Heading1"))
|
||||
md.append("# ").append(text).append("\n");
|
||||
else if (style.startsWith("Heading_20_2") || style.startsWith("Heading2"))
|
||||
md.append("## ").append(text).append("\n");
|
||||
else if (style.startsWith("Heading_20_3") || style.startsWith("Heading3"))
|
||||
md.append("### ").append(text).append("\n");
|
||||
else
|
||||
md.append(text).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return md.toString().trim();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Leitet MIME-Typ aus Dateiendung ab, wenn der gespeicherte MIME-Typ
|
||||
* fehlt oder generic ist.
|
||||
*/
|
||||
private String resolveMimeType(String mimeType, String filename) {
|
||||
if (mimeType != null && !mimeType.isBlank()
|
||||
&& !mimeType.equals("application/octet-stream")) {
|
||||
return mimeType;
|
||||
}
|
||||
if (filename == null) return "application/octet-stream";
|
||||
String lower = filename.toLowerCase();
|
||||
if (lower.endsWith(".pdf")) return "application/pdf";
|
||||
if (lower.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
if (lower.endsWith(".doc")) return "application/msword";
|
||||
if (lower.endsWith(".odt")) return "application/vnd.oasis.opendocument.text";
|
||||
if (lower.endsWith(".txt")) return "text/plain";
|
||||
if (lower.endsWith(".md")) return "text/markdown";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package de.frigosped.dc.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.frigosped.dc.model.EvaluationResult;
|
||||
import de.frigosped.dc.model.OrdsQuestion;
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.ChatLanguageModel;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* KI-gestützte Auswertung:
|
||||
* 1. Übersetzung eines Dokuments ins Deutsche
|
||||
* 2. Prüfung des Dokuments gegen eine einzelne Frage
|
||||
*
|
||||
* Verwendet das Hauptmodell (gpt-oss:20b).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EvaluationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EvaluationService.class);
|
||||
|
||||
/** Regex zum Extrahieren von JSON aus der KI-Antwort (toleriert Markdown-Codeblöcke) */
|
||||
private static final Pattern JSON_PATTERN =
|
||||
Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```|([{][\\s\\S]*[}])",
|
||||
Pattern.DOTALL);
|
||||
|
||||
@Inject
|
||||
@Named("main")
|
||||
ChatLanguageModel mainModel;
|
||||
|
||||
@Inject
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
// =========================================================================
|
||||
// Übersetzung
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Übersetzt einen Markdown-Text aus der Quellsprache ins Deutsche.
|
||||
* Wenn der Text bereits auf Deutsch ist, wird er unverändert zurückgegeben.
|
||||
*
|
||||
* @param originalText Markdown-Text in Originalsprache
|
||||
* @return Markdown-Text auf Deutsch
|
||||
*/
|
||||
public String translate(String originalText) {
|
||||
if (originalText == null || originalText.isBlank()) return "";
|
||||
LOG.debugf("Starte Übersetzung (%d Zeichen)...", originalText.length());
|
||||
|
||||
List<ChatMessage> messages = List.of(
|
||||
SystemMessage.from(
|
||||
"Du bist ein professioneller Übersetzer für rechtliche und logistische Dokumente.\n" +
|
||||
"Übersetze den folgenden Text ins Deutsche.\n" +
|
||||
"Regeln:\n" +
|
||||
"- Behalte die Markdown-Formatierung exakt bei\n" +
|
||||
"- Fachbegriffe korrekt übersetzen\n" +
|
||||
"- Wenn der Text bereits auf Deutsch ist, gib ihn unverändert zurück\n" +
|
||||
"- Antworte NUR mit dem übersetzten Text, ohne Kommentar oder Präambel"
|
||||
),
|
||||
UserMessage.from(originalText)
|
||||
);
|
||||
|
||||
Response<AiMessage> response = mainModel.generate(messages);
|
||||
return response.content().text().trim();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Fragen-Auswertung
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Wertet eine einzelne Frage gegen den Dokument-Text aus.
|
||||
*
|
||||
* Der Schwellwert (threshold) und die Abweichungs-Logik werden
|
||||
* im Orchestrator berechnet – die KI liefert nur score + answer + comment.
|
||||
*
|
||||
* @param question Frage aus dem Katalog (inkl. Beispiele, Schwellwert)
|
||||
* @param documentText Originaltext des Dokuments (Markdown)
|
||||
* @return Strukturiertes Auswertungsergebnis
|
||||
*/
|
||||
public EvaluationResult evaluate(OrdsQuestion question, String documentText) {
|
||||
LOG.debugf("Werte Frage %d aus: %s",
|
||||
question.getQuestionId(),
|
||||
abbreviate(question.getQuestionText(), 80));
|
||||
|
||||
String systemPrompt = buildEvaluationSystem();
|
||||
String userPrompt = buildEvaluationUser(question, documentText);
|
||||
|
||||
List<ChatMessage> messages = List.of(
|
||||
SystemMessage.from(systemPrompt),
|
||||
UserMessage.from(userPrompt)
|
||||
);
|
||||
|
||||
Response<AiMessage> response = mainModel.generate(messages);
|
||||
String rawText = response.content().text();
|
||||
|
||||
return parseEvaluationResult(rawText, question);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Prompt-Builder
|
||||
// =========================================================================
|
||||
|
||||
private String buildEvaluationSystem() {
|
||||
return """
|
||||
Du bist ein Experte für Transportrecht, AGB-Recht und Vertragsanalyse.
|
||||
Du prüfst Transport- und Speditionsdokumente gegen einen Fragenkatalog.
|
||||
|
||||
Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Objekt – kein Text davor oder danach.
|
||||
Das JSON-Objekt muss folgende Felder enthalten:
|
||||
|
||||
{
|
||||
"answer": "<Deine Antwort auf Deutsch. Zitate aus dem Dokument in Originalsprache angeben und danach in Klammern übersetzen>",
|
||||
"score": <Zahl 0 bis 100, wie gut das Dokument die Anforderung erfüllt>,
|
||||
"result_type": "<OK wenn score >= Schwellwert, UNKLAR wenn grenzwertig, NOK wenn klar nicht erfüllt>",
|
||||
"the_comment": "<Kurzer fachlicher Kommentar auf Deutsch, max. 3 Sätze>"
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private String buildEvaluationUser(OrdsQuestion question, String documentText) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("## Zu prüfendes Dokument\n\n");
|
||||
sb.append(documentText).append("\n\n");
|
||||
|
||||
sb.append("---\n\n");
|
||||
sb.append("## Prüffrage (Kategorie: ").append(question.getCategoryName()).append(")\n\n");
|
||||
sb.append(question.getQuestionText()).append("\n\n");
|
||||
|
||||
if (question.getThreshold() != null) {
|
||||
sb.append("**Schwellwert:** ").append(question.getThreshold())
|
||||
.append("% – unterhalb gilt die Anforderung als nicht erfüllt\n\n");
|
||||
}
|
||||
|
||||
if (question.getExample0Percent() != null && !question.getExample0Percent().isBlank()) {
|
||||
sb.append("**Beispiel: 0% Erfüllung (nicht ok):**\n")
|
||||
.append(question.getExample0Percent()).append("\n\n");
|
||||
}
|
||||
|
||||
if (question.getExample100Percent() != null && !question.getExample100Percent().isBlank()) {
|
||||
sb.append("**Beispiel: 100% Erfüllung (vollständig ok):**\n")
|
||||
.append(question.getExample100Percent()).append("\n\n");
|
||||
}
|
||||
|
||||
sb.append("---\n\nAntworte jetzt mit dem JSON-Objekt:");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// JSON-Parsing mit Fallback
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Parst das KI-Ergebnis zu einem EvaluationResult.
|
||||
* Toleriert Markdown-Codeblöcke und leichte Formatierungsfehler.
|
||||
*/
|
||||
private EvaluationResult parseEvaluationResult(String rawText, OrdsQuestion question) {
|
||||
try {
|
||||
String json = extractJson(rawText);
|
||||
// Jackson liest snake_case dank quarkus.jackson.property-naming-strategy=SNAKE_CASE
|
||||
EvaluationResult result = objectMapper.readValue(json, EvaluationResult.class);
|
||||
|
||||
// Validierung + Defaults
|
||||
if (result.getScore() == null) result.setScore(0);
|
||||
if (result.getResultType() == null || !isValidResultType(result.getResultType())) {
|
||||
result.setResultType(deriveResultType(result.getScore(), question.getThreshold()));
|
||||
}
|
||||
if (result.getAnswer() == null) result.setAnswer("Keine Antwort erhalten.");
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("JSON-Parsing fehlgeschlagen für Frage %d, verwende Fallback. Fehler: %s",
|
||||
question.getQuestionId(), e.getMessage());
|
||||
LOG.debugf("Rohe KI-Antwort: %s", rawText);
|
||||
|
||||
// Fallback: UNKLAR mit dem Rohtext als Antwort
|
||||
EvaluationResult fallback = new EvaluationResult();
|
||||
fallback.setAnswer(rawText != null ? rawText.trim() : "Parsing-Fehler");
|
||||
fallback.setScore(0);
|
||||
fallback.setResultType("UNKLAR");
|
||||
fallback.setTheComment("Automatische Auswertung konnte nicht strukturiert werden.");
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert den JSON-Teil aus der KI-Antwort
|
||||
* (toleriert ```json ... ``` Blöcke oder reines JSON).
|
||||
*/
|
||||
private String extractJson(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
throw new IllegalArgumentException("Leere KI-Antwort");
|
||||
}
|
||||
|
||||
// Versuch 1: JSON in ```-Block
|
||||
Matcher m = JSON_PATTERN.matcher(text);
|
||||
if (m.find()) {
|
||||
String candidate = m.group(1) != null ? m.group(1) : m.group(2);
|
||||
if (candidate != null && !candidate.isBlank()) {
|
||||
return candidate.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Versuch 2: Alles ab erstem { bis letztem }
|
||||
int start = text.indexOf('{');
|
||||
int end = text.lastIndexOf('}');
|
||||
if (start >= 0 && end > start) {
|
||||
return text.substring(start, end + 1);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Kein JSON-Objekt in KI-Antwort gefunden");
|
||||
}
|
||||
|
||||
private boolean isValidResultType(String type) {
|
||||
return "OK".equals(type) || "UNKLAR".equals(type) || "NOK".equals(type);
|
||||
}
|
||||
|
||||
private String deriveResultType(int score, Integer threshold) {
|
||||
if (threshold == null) threshold = 70;
|
||||
if (score >= threshold) return "OK";
|
||||
if (score >= threshold / 2) return "UNKLAR";
|
||||
return "NOK";
|
||||
}
|
||||
|
||||
private String abbreviate(String s, int maxLen) {
|
||||
if (s == null) return "";
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "…";
|
||||
}
|
||||
}
|
||||
64
dc-backend/src/main/resources/application.properties
Normal file
64
dc-backend/src/main/resources/application.properties
Normal file
@@ -0,0 +1,64 @@
|
||||
# =============================================================================
|
||||
# Dokumenten-Check Backend – Konfiguration
|
||||
# =============================================================================
|
||||
|
||||
quarkus.application.name=dc-backend
|
||||
quarkus.http.port=8090
|
||||
|
||||
# =============================================================================
|
||||
# API-Key Absicherung (Env: DC_API_KEY)
|
||||
# Leer = Auth deaktiviert (nur für lokale Entwicklung)
|
||||
# =============================================================================
|
||||
dc.api.key=${DC_API_KEY:}
|
||||
|
||||
# =============================================================================
|
||||
# KI-Modelle (Ollama-kompatibler Endpunkt)
|
||||
# =============================================================================
|
||||
dc.ai.base-url=https://ollama.aquantico.de
|
||||
dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
|
||||
|
||||
# Hauptmodell: Auswertung + Übersetzung
|
||||
dc.ai.main.model=qwen3.6:35b-a3b-q4_K_M
|
||||
dc.ai.main.timeout=300s
|
||||
dc.ai.main.max-retries=2
|
||||
dc.ai.main.temperature=0.1
|
||||
|
||||
# OCR-Modell: Bildtext-Extraktion aus PDF-Seiten
|
||||
dc.ai.ocr.model=qwen3.6:35b-a3b-q4_K_M
|
||||
dc.ai.ocr.timeout=120s
|
||||
dc.ai.ocr.max-retries=2
|
||||
|
||||
# =============================================================================
|
||||
# ORDS REST Client
|
||||
# =============================================================================
|
||||
# Basis-URL des ORDS-Servers (Schema-Pfad anpassen)
|
||||
quarkus.rest-client.ords-api.url=http://ords:8080/ords/ai_dev
|
||||
quarkus.rest-client.ords-api.scope=jakarta.inject.Singleton
|
||||
quarkus.rest-client.ords-api.connect-timeout=10000
|
||||
quarkus.rest-client.ords-api.read-timeout=60000
|
||||
|
||||
# ORDS-Authentifizierung (OAuth2 Bearer Token – leer = kein Auth in Dev)
|
||||
dc.ords.bearer-token=
|
||||
|
||||
# =============================================================================
|
||||
# Logging
|
||||
# =============================================================================
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."de.frigosped".level=DEBUG
|
||||
|
||||
# KI-Kommunikation mitloggen (nur in Dev)
|
||||
%dev.dc.ai.log-requests=true
|
||||
%dev.dc.ai.log-responses=true
|
||||
%prod.dc.ai.log-requests=false
|
||||
%prod.dc.ai.log-responses=false
|
||||
|
||||
# =============================================================================
|
||||
# Jackson: snake_case für ORDS-Responses
|
||||
# =============================================================================
|
||||
quarkus.jackson.property-naming-strategy=SNAKE_CASE
|
||||
|
||||
# =============================================================================
|
||||
# OpenAPI
|
||||
# =============================================================================
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
quarkus.swagger-ui.always-include=true
|
||||
1
dc-backend/target/build-metrics.json
Normal file
1
dc-backend/target/build-metrics.json
Normal file
File diff suppressed because one or more lines are too long
64
dc-backend/target/classes/application.properties
Normal file
64
dc-backend/target/classes/application.properties
Normal file
@@ -0,0 +1,64 @@
|
||||
# =============================================================================
|
||||
# Dokumenten-Check Backend – Konfiguration
|
||||
# =============================================================================
|
||||
|
||||
quarkus.application.name=dc-backend
|
||||
quarkus.http.port=8090
|
||||
|
||||
# =============================================================================
|
||||
# API-Key Absicherung (Env: DC_API_KEY)
|
||||
# Leer = Auth deaktiviert (nur für lokale Entwicklung)
|
||||
# =============================================================================
|
||||
dc.api.key=${DC_API_KEY:}
|
||||
|
||||
# =============================================================================
|
||||
# KI-Modelle (Ollama-kompatibler Endpunkt)
|
||||
# =============================================================================
|
||||
dc.ai.base-url=https://ollama.aquantico.de
|
||||
dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
|
||||
|
||||
# Hauptmodell: Auswertung + Übersetzung
|
||||
dc.ai.main.model=gpt-oss:20b
|
||||
dc.ai.main.timeout=300s
|
||||
dc.ai.main.max-retries=2
|
||||
dc.ai.main.temperature=0.1
|
||||
|
||||
# OCR-Modell: Bildtext-Extraktion aus PDF-Seiten
|
||||
dc.ai.ocr.model=quen3.5:9b
|
||||
dc.ai.ocr.timeout=120s
|
||||
dc.ai.ocr.max-retries=2
|
||||
|
||||
# =============================================================================
|
||||
# ORDS REST Client
|
||||
# =============================================================================
|
||||
# Basis-URL des ORDS-Servers (Schema-Pfad anpassen)
|
||||
quarkus.rest-client.ords-api.url=http://localhost:8080/ords/FRIGOSPED_APP
|
||||
quarkus.rest-client.ords-api.scope=jakarta.inject.Singleton
|
||||
quarkus.rest-client.ords-api.connect-timeout=10000
|
||||
quarkus.rest-client.ords-api.read-timeout=60000
|
||||
|
||||
# ORDS-Authentifizierung (OAuth2 Bearer Token – leer = kein Auth in Dev)
|
||||
dc.ords.bearer-token=
|
||||
|
||||
# =============================================================================
|
||||
# Logging
|
||||
# =============================================================================
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."de.frigosped".level=DEBUG
|
||||
|
||||
# KI-Kommunikation mitloggen (nur in Dev)
|
||||
%dev.dc.ai.log-requests=true
|
||||
%dev.dc.ai.log-responses=true
|
||||
%prod.dc.ai.log-requests=false
|
||||
%prod.dc.ai.log-responses=false
|
||||
|
||||
# =============================================================================
|
||||
# Jackson: snake_case für ORDS-Responses
|
||||
# =============================================================================
|
||||
quarkus.jackson.property-naming-strategy=SNAKE_CASE
|
||||
|
||||
# =============================================================================
|
||||
# OpenAPI
|
||||
# =============================================================================
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
quarkus.swagger-ui.always-include=true
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
de/frigosped/dc/service/CheckOrchestrationService.class
|
||||
de/frigosped/dc/service/EvaluationService.class
|
||||
de/frigosped/dc/service/DocumentProcessingService.class
|
||||
de/frigosped/dc/security/ApiKeyFilter.class
|
||||
@@ -0,0 +1,16 @@
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/EvaluationResult.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocument.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocumentList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsProject.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestion.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestionList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ProgressRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ResultRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/TextsRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/security/ApiKeyFilter.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
|
||||
@@ -0,0 +1,12 @@
|
||||
de/frigosped/dc/model/OrdsQuestion.class
|
||||
de/frigosped/dc/model/OrdsQuestionList.class
|
||||
de/frigosped/dc/model/ProgressRequest.class
|
||||
de/frigosped/dc/client/OrdsClient.class
|
||||
de/frigosped/dc/ai/AiModelProducer.class
|
||||
de/frigosped/dc/model/OrdsDocument.class
|
||||
de/frigosped/dc/model/OrdsProject.class
|
||||
de/frigosped/dc/model/OrdsDocumentList.class
|
||||
de/frigosped/dc/model/TextsRequest.class
|
||||
de/frigosped/dc/model/ResultRequest.class
|
||||
de/frigosped/dc/model/EvaluationResult.class
|
||||
de/frigosped/dc/resource/CheckResource.class
|
||||
@@ -0,0 +1,15 @@
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/EvaluationResult.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocument.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsDocumentList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsProject.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestion.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/OrdsQuestionList.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ProgressRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ResultRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/TextsRequest.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
|
||||
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
|
||||
33
dc-backend/update.sh
Normal file
33
dc-backend/update.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
export KUBECONFIG="$SCRIPT_DIR/../env/frigo-dev.yaml"
|
||||
|
||||
TAG="${1:-latest}"
|
||||
|
||||
echo "=== dc-backend Update ==="
|
||||
echo ""
|
||||
|
||||
# --- Build & Push ---
|
||||
echo "→ [1/3] Build & Push Image (Tag: $TAG)..."
|
||||
bash "$SCRIPT_DIR/build-and-push.sh" "$TAG"
|
||||
|
||||
# --- Apply Kubernetes Manifests ---
|
||||
echo ""
|
||||
echo "→ [2/3] Kubernetes Manifests anwenden..."
|
||||
kubectl apply -f "$SCRIPT_DIR/k8s/deployment.yaml"
|
||||
|
||||
# --- Rolling Restart (zieht neues 'latest'-Image) ---
|
||||
echo ""
|
||||
echo "→ [3/3] Rolling Restart..."
|
||||
kubectl rollout restart deployment/dc-backend -n ai-env
|
||||
|
||||
echo ""
|
||||
echo "→ Warte auf Rollout..."
|
||||
kubectl rollout status deployment/dc-backend -n ai-env --timeout=120s
|
||||
|
||||
echo ""
|
||||
echo "✓ Deployment abgeschlossen."
|
||||
echo ""
|
||||
kubectl get pods -n ai-env -l app=dc-backend
|
||||
Reference in New Issue
Block a user