Beispiele und anpassungen an Mistral

This commit is contained in:
Wolf G. Beckmann
2026-06-10 14:21:59 +02:00
parent f3ae6859bf
commit d00525c15f
44 changed files with 676 additions and 55 deletions

View File

@@ -32,10 +32,20 @@ spec:
name: dc-backend-secrets
key: api-key
optional: true
# Ollama KI-Endpunkt
- name: DC_AI_BASE_URL
# KI-Provider: mistral | ollama
- name: DC_AI_PROVIDER
value: "mistral"
# Mistral API-Key
- name: MISTRAL_API_KEY
valueFrom:
secretKeyRef:
name: dc-backend-secrets
key: mistral-api-key
optional: true
# Ollama-Fallback (aktiv wenn DC_AI_PROVIDER=ollama)
- name: DC_AI_OLLAMA_BASE_URL
value: "https://ollama.aquantico.de"
- name: DC_AI_API_KEY
- name: DC_AI_OLLAMA_API_KEY
valueFrom:
secretKeyRef:
name: dc-backend-secrets
@@ -85,4 +95,5 @@ metadata:
type: Opaque
stringData:
api-key: ""
mistral-api-key: "2AkoohJC8MjfjhfLGObQESZs9jxbDnc0"
ollama-api-key: "324GF44-50AA-4B57-9386-K435DLJ764DFR"

View File

@@ -9,6 +9,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class EvaluationResult {
/** Frage-ID wird im Batch-Modus von der KI zurückgegeben */
private Long questionId;
/** Antwort auf Deutsch (Zitate in Originalsprache + Übersetzung) */
private String answer;
@@ -23,15 +26,18 @@ public class EvaluationResult {
public EvaluationResult() {}
public String getAnswer() { return answer; }
public void setAnswer(String v) { this.answer = v; }
public Long getQuestionId() { return questionId; }
public void setQuestionId(Long v) { this.questionId = v; }
public Integer getScore() { return score; }
public void setScore(Integer v) { this.score = v; }
public String getAnswer() { return answer; }
public void setAnswer(String v) { this.answer = v; }
public String getResultType() { return resultType; }
public void setResultType(String v) { this.resultType = v; }
public Integer getScore() { return score; }
public void setScore(Integer v) { this.score = v; }
public String getTheComment() { return theComment; }
public void setTheComment(String v) { this.theComment = 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; }
}

View File

@@ -13,6 +13,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -195,54 +196,33 @@ public class CheckOrchestrationService {
continue;
}
for (OrdsQuestion question : questions) {
// Vor dem Schritt: Status auf "wertet Frage X in Dokument Y aus"
List<List<OrdsQuestion>> batches = partition(questions, 4);
for (List<OrdsQuestion> batch : batches) {
OrdsQuestion first = batch.get(0);
pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
doc.getId(), question.getQuestionId(), startedAt);
doc.getId(), first.getQuestionId(), startedAt);
Map<Long, EvaluationResult> batchResults;
try {
LOG.debugf("Auswertung: Dok %d × Frage %d",
doc.getId(), question.getQuestionId());
EvaluationResult result = evaluationService.evaluate(question, originalText, projectId);
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());
}
LOG.debugf("Batch-Auswertung: Dok %d, %d Fragen ab ID %d",
(Object) doc.getId(), (Object) batch.size(), (Object) first.getQuestionId());
batchResults = evaluationService.evaluateBatch(batch, originalText, projectId);
} catch (Exception e) {
LOG.errorf(e, "Auswertungsfehler: Frage %d / Dokument %d",
question.getQuestionId(), doc.getId());
LOG.warnf("Batch fehlgeschlagen (Dok %d, Frage %d+) falle auf Einzelaufrufe zurück: %s",
doc.getId(), first.getQuestionId(), e.getMessage());
batchResults = evaluateIndividually(batch, originalText, projectId);
}
// Nach dem Schritt: Fortschritt + ETA aktualisieren
stepHolder[0]++;
for (OrdsQuestion question : batch) {
EvaluationResult result = batchResults.get(question.getQuestionId());
if (result == null) continue;
saveResult(projectId, doc.getId(), question, result);
}
stepHolder[0] += batch.size();
pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
doc.getId(), question.getQuestionId(), startedAt);
doc.getId(), batch.get(batch.size() - 1).getQuestionId(), startedAt);
}
}
@@ -277,6 +257,56 @@ public class CheckOrchestrationService {
}
}
/** Teilt eine Liste in Untergruppen der gewünschten Größe auf. */
private static <T> List<List<T>> partition(List<T> list, int size) {
List<List<T>> parts = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
parts.add(list.subList(i, Math.min(i + size, list.size())));
}
return parts;
}
/** Fallback: wertet jede Frage einzeln aus, gibt Map question_id → result zurück. */
private Map<Long, EvaluationResult> evaluateIndividually(List<OrdsQuestion> questions,
String documentText,
long projectId) {
Map<Long, EvaluationResult> map = new HashMap<>();
for (OrdsQuestion q : questions) {
try {
map.put(q.getQuestionId(),
evaluationService.evaluate(q, documentText, projectId));
} catch (Exception e) {
LOG.errorf(e, "Einzelauswertung fehlgeschlagen: Frage %d", q.getQuestionId());
}
}
return map;
}
private void saveResult(long projectId, long docId,
OrdsQuestion question, EvaluationResult result) {
try {
int deviation = 0, 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(), docId,
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(), docId);
}
} catch (Exception e) {
LOG.errorf(e, "Ergebnis-Speichern fehlgeschlagen: Frage %d / Dokument %d",
question.getQuestionId(), docId);
}
}
/**
* Berechnet die geschätzte Fertigstellung auf Basis der bisher verstrichenen Zeit.
* ETA = jetzt + (verbleibende Schritte × Ø Dauer pro Schritt)

View File

@@ -14,7 +14,9 @@ import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jboss.logging.Logger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -113,6 +115,157 @@ public class EvaluationService {
return parseEvaluationResult(rawText, question);
}
// =========================================================================
// Batch-Auswertung (bis zu 4 Fragen pro KI-Aufruf)
// =========================================================================
/**
* Wertet mehrere Fragen in einem einzigen KI-Aufruf aus.
* Gibt eine Map question_id → EvaluationResult zurück.
* Wirft eine Exception wenn die KI-Antwort nicht als JSON-Array parsbar ist,
* damit der Aufrufer auf Einzel-Aufrufe zurückfallen kann.
*/
public Map<Long, EvaluationResult> evaluateBatch(List<OrdsQuestion> questions,
String documentText,
long projectId) {
if (questions.isEmpty()) return Map.of();
LOG.debugf("Batch-Auswertung: %d Frage(n)", questions.size());
List<ChatMessage> messages = List.of(
SystemMessage.from(buildBatchSystem(questions.size())),
UserMessage.from(buildBatchUser(questions, documentText))
);
Response<AiMessage> response = mainModel.generate(messages);
costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());
List<EvaluationResult> results = parseBatchResult(response.content().text(), questions);
Map<Long, EvaluationResult> map = new HashMap<>();
for (EvaluationResult r : results) {
if (r.getQuestionId() != null) {
map.put(r.getQuestionId(), r);
}
}
// Lücken füllen: für fehlende question_ids Fallback-Ergebnis eintragen
for (OrdsQuestion q : questions) {
map.computeIfAbsent(q.getQuestionId(), id -> {
LOG.warnf("Keine Batch-Antwort für Frage %d verwende UNKLAR-Fallback", id);
return buildFallback("Frage wurde im Batch nicht beantwortet.");
});
}
return map;
}
private String buildBatchSystem(int questionCount) {
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-Array kein Text davor oder danach.
Das Array enthält für JEDE Frage genau ein Objekt:
[
{
"question_id": <ID der Frage exakt wie angegeben>,
"answer": "<Antwort auf Deutsch; Zitate aus dem Dokument in Originalsprache mit Übersetzung in Klammern>",
"score": <Erfüllungsgrad 0 bis 100>,
"result_type": "<OK wenn score >= Schwellwert, NOK wenn klar nicht erfüllt, UNKLAR wenn grenzwertig>",
"the_comment": "<Kurzer fachlicher Kommentar auf Deutsch, max. 3 Sätze>"
}
]
Pflicht: Das Array muss GENAU %d Einträge enthalten (einen pro Frage).
Die question_id jedes Eintrags muss exakt der ID der gestellten Frage entsprechen.
""".formatted(questionCount);
}
private String buildBatchUser(List<OrdsQuestion> questions, String documentText) {
StringBuilder sb = new StringBuilder();
sb.append("## Zu prüfendes Dokument\n\n");
sb.append(documentText).append("\n\n---\n\n");
sb.append("## Prüffragen (").append(questions.size()).append(" Fragen)\n\n");
for (int i = 0; i < questions.size(); i++) {
OrdsQuestion q = questions.get(i);
sb.append("### Frage ").append(i + 1)
.append(" (ID: ").append(q.getQuestionId())
.append(", Kategorie: ").append(q.getCategoryName()).append(")\n\n");
sb.append(q.getQuestionText()).append("\n\n");
if (q.getThreshold() != null) {
sb.append("**Schwellwert:** ").append(q.getThreshold())
.append("% unterhalb gilt die Anforderung als nicht erfüllt\n\n");
}
if (q.getExample0Percent() != null && !q.getExample0Percent().isBlank()) {
sb.append("**Beispiel 0%:** ").append(q.getExample0Percent()).append("\n\n");
}
if (q.getExample100Percent() != null && !q.getExample100Percent().isBlank()) {
sb.append("**Beispiel 100%:** ").append(q.getExample100Percent()).append("\n\n");
}
}
sb.append("---\n\nAntworte jetzt mit dem JSON-Array (").append(questions.size())
.append(" Einträge):");
return sb.toString();
}
private List<EvaluationResult> parseBatchResult(String rawText, List<OrdsQuestion> questions) {
try {
String json = extractJsonArray(rawText);
List<EvaluationResult> results = objectMapper.readValue(
json,
objectMapper.getTypeFactory().constructCollectionType(List.class, EvaluationResult.class)
);
// Fehlende Defaults setzen
for (int i = 0; i < results.size(); i++) {
EvaluationResult r = results.get(i);
if (r.getScore() == null) r.setScore(0);
if (r.getAnswer() == null) r.setAnswer("Keine Antwort erhalten.");
// question_id aus Position ableiten falls KI sie vergessen hat
if (r.getQuestionId() == null && i < questions.size()) {
r.setQuestionId(questions.get(i).getQuestionId());
}
if (r.getResultType() == null || !isValidResultType(r.getResultType())) {
Integer threshold = (i < questions.size()) ? questions.get(i).getThreshold() : null;
r.setResultType(deriveResultType(r.getScore(), threshold));
}
}
return results;
} catch (Exception e) {
LOG.warnf("Batch-JSON-Parsing fehlgeschlagen: %s", e.getMessage());
LOG.debugf("Rohe KI-Antwort: %s", rawText);
throw new RuntimeException("Batch-Parsing fehlgeschlagen: " + e.getMessage(), e);
}
}
private String extractJsonArray(String text) {
if (text == null || text.isBlank()) throw new IllegalArgumentException("Leere KI-Antwort");
// 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.stripLeading().startsWith("[")) return candidate.trim();
}
// Direkt als Array
int start = text.indexOf('[');
int end = text.lastIndexOf(']');
if (start >= 0 && end > start) return text.substring(start, end + 1);
throw new IllegalArgumentException("Kein JSON-Array in KI-Antwort gefunden");
}
private EvaluationResult buildFallback(String message) {
EvaluationResult r = new EvaluationResult();
r.setAnswer(message);
r.setScore(0);
r.setResultType("UNKLAR");
r.setTheComment("Automatische Auswertung konnte nicht zugeordnet werden.");
return r;
}
// =========================================================================
// Prompt-Builder
// =========================================================================