Beispiele und anpassungen an Mistral
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user