Erweiterungen
This commit is contained in:
@@ -17,7 +17,7 @@ import java.net.URL;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class LlmConfig {
|
public class LlmConfig {
|
||||||
|
|
||||||
@Value("${spring.ai.openai.base-url:http://192.168.180.3:1234}")
|
@Value("${spring.ai.openai.base-url:https://lmstudio.appcreation.de}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
||||||
@Value("${spring.ai.openai.chat.options.model:local-model}")
|
@Value("${spring.ai.openai.chat.options.model:local-model}")
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class LlmRestClient {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final String model;
|
private final String model;
|
||||||
|
|
||||||
public LlmRestClient(@Value("${spring.ai.openai.base-url:http://192.168.180.3:1234}") String baseUrl,
|
public LlmRestClient(@Value("${spring.ai.openai.base-url:https://lmstudio.appcreation.de}") String baseUrl,
|
||||||
@Value("${spring.ai.openai.chat.options.model:local-model}") String model, ObjectMapper objectMapper) {
|
@Value("${spring.ai.openai.chat.options.model:local-model}") String model, ObjectMapper objectMapper) {
|
||||||
|
|
||||||
this.webClient = WebClient.builder().baseUrl(baseUrl + "/v1/chat/completions").build();
|
this.webClient = WebClient.builder().baseUrl(baseUrl + "/v1/chat/completions").build();
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
// Force recompile
|
// Force recompile
|
||||||
|
|
||||||
@@ -69,21 +71,32 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the payload and adds translations for job remarks, task
|
* Collects all translatable texts from the payload, fetches all translations in
|
||||||
* descriptions/texts, and cargo item descriptions.
|
* one batch (at most one LLM call), then applies them to the JSON tree.
|
||||||
*/
|
*/
|
||||||
private Object processPayloadWithTranslations(Object payload) {
|
private Object processPayloadWithTranslations(Object payload) {
|
||||||
try {
|
try {
|
||||||
// Handle single JobWithRelatedDataDTO
|
|
||||||
if (payload instanceof JobWithRelatedDataDTO dto) {
|
if (payload instanceof JobWithRelatedDataDTO dto) {
|
||||||
return convertToTranslatedJson(dto);
|
List<String> texts = collectTexts(dto);
|
||||||
|
Map<String, List<TranslationService.Translation>> translations = translationService
|
||||||
|
.translateBatch(texts);
|
||||||
|
return convertToTranslatedJson(dto, translations);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle list of JobWithRelatedDataDTO
|
if (payload instanceof List<?> list && !list.isEmpty()
|
||||||
if (payload instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof JobWithRelatedDataDTO) {
|
&& list.get(0) instanceof JobWithRelatedDataDTO) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<JobWithRelatedDataDTO> dtoList = (List<JobWithRelatedDataDTO>) list;
|
List<JobWithRelatedDataDTO> dtoList = (List<JobWithRelatedDataDTO>) list;
|
||||||
return dtoList.stream().map(this::convertToTranslatedJson).toList();
|
|
||||||
|
// Collect all texts from all DTOs and translate in one batch
|
||||||
|
List<String> allTexts = dtoList.stream()
|
||||||
|
.flatMap(d -> collectTexts(d).stream())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
Map<String, List<TranslationService.Translation>> translations = translationService
|
||||||
|
.translateBatch(allTexts);
|
||||||
|
|
||||||
|
return dtoList.stream().map(d -> convertToTranslatedJson(d, translations)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
@@ -94,53 +107,86 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts JobWithRelatedDataDTO to JSON with translated fields.
|
* Collects all non-blank translatable strings from a DTO.
|
||||||
*/
|
*/
|
||||||
private ObjectNode convertToTranslatedJson(JobWithRelatedDataDTO dto) {
|
private List<String> collectTexts(JobWithRelatedDataDTO dto) {
|
||||||
// Convert to JSON tree
|
List<String> texts = new ArrayList<>();
|
||||||
ObjectNode root = objectMapper.valueToTree(dto);
|
|
||||||
|
|
||||||
// Translate job remark
|
if (dto.getJob() != null && isNonBlank(dto.getJob().getRemark())) {
|
||||||
if (dto.getJob() != null && dto.getJob().getRemark() != null && !dto.getJob().getRemark().isBlank()) {
|
texts.add(dto.getJob().getRemark());
|
||||||
List<TranslationService.Translation> translations = translationService
|
|
||||||
.translateToAllLanguages(dto.getJob().getRemark());
|
|
||||||
root.withObject("job").set("remark", createTranslationArray(translations));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate task descriptions, displayNames and button texts
|
if (dto.getTasks() != null) {
|
||||||
|
for (BaseTask task : dto.getTasks()) {
|
||||||
|
if (isNonBlank(task.getDescription())) {
|
||||||
|
texts.add(task.getDescription());
|
||||||
|
}
|
||||||
|
if (isNonBlank(task.getDisplayName())) {
|
||||||
|
texts.add(task.getDisplayName());
|
||||||
|
}
|
||||||
|
if (task instanceof ConfirmationTask ct && isNonBlank(ct.getButtonText())) {
|
||||||
|
texts.add(ct.getButtonText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.getCargoItems() != null) {
|
||||||
|
for (CargoItem item : dto.getCargoItems()) {
|
||||||
|
if (isNonBlank(item.getDescription())) {
|
||||||
|
texts.add(item.getDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return texts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a DTO to a JSON tree and replaces translatable string fields with
|
||||||
|
* translation arrays, using the pre-fetched translation map.
|
||||||
|
*/
|
||||||
|
private ObjectNode convertToTranslatedJson(JobWithRelatedDataDTO dto,
|
||||||
|
Map<String, List<TranslationService.Translation>> translations) {
|
||||||
|
|
||||||
|
ObjectNode root = objectMapper.valueToTree(dto);
|
||||||
|
|
||||||
|
// Job remark
|
||||||
|
if (dto.getJob() != null && isNonBlank(dto.getJob().getRemark())) {
|
||||||
|
List<TranslationService.Translation> t = translations.get(dto.getJob().getRemark());
|
||||||
|
if (t != null) {
|
||||||
|
root.withObject("job").set("remark", createTranslationArray(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks
|
||||||
if (dto.getTasks() != null && !dto.getTasks().isEmpty()) {
|
if (dto.getTasks() != null && !dto.getTasks().isEmpty()) {
|
||||||
ArrayNode tasksNode = root.withArray("tasks");
|
ArrayNode tasksNode = root.withArray("tasks");
|
||||||
for (int i = 0; i < dto.getTasks().size(); i++) {
|
for (int i = 0; i < dto.getTasks().size(); i++) {
|
||||||
BaseTask task = dto.getTasks().get(i);
|
BaseTask task = dto.getTasks().get(i);
|
||||||
ObjectNode taskNode = (ObjectNode) tasksNode.get(i);
|
ObjectNode taskNode = (ObjectNode) tasksNode.get(i);
|
||||||
|
|
||||||
// Translate description
|
if (isNonBlank(task.getDescription())) {
|
||||||
if (task.getDescription() != null && !task.getDescription().isBlank()) {
|
List<TranslationService.Translation> t = translations.get(task.getDescription());
|
||||||
List<TranslationService.Translation> translations = translationService
|
if (t != null) {
|
||||||
.translateToAllLanguages(task.getDescription());
|
taskNode.set("description", createTranslationArray(t));
|
||||||
taskNode.set("description", createTranslationArray(translations));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate displayName
|
if (isNonBlank(task.getDisplayName())) {
|
||||||
if (task.getDisplayName() != null && !task.getDisplayName().isBlank()) {
|
List<TranslationService.Translation> t = translations.get(task.getDisplayName());
|
||||||
List<TranslationService.Translation> translations = translationService
|
if (t != null) {
|
||||||
.translateToAllLanguages(task.getDisplayName());
|
taskNode.set("displayName", createTranslationArray(t));
|
||||||
taskNode.set("displayName", createTranslationArray(translations));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate buttonText in taskSpecificData for ConfirmationTask
|
if (task instanceof ConfirmationTask ct && isNonBlank(ct.getButtonText())) {
|
||||||
if (task instanceof ConfirmationTask confirmationTask) {
|
List<TranslationService.Translation> t = translations.get(ct.getButtonText());
|
||||||
if (confirmationTask.getButtonText() != null && !confirmationTask.getButtonText().isBlank()) {
|
if (t != null) {
|
||||||
// Translate buttonText at task level
|
taskNode.set("buttonText", createTranslationArray(t));
|
||||||
List<TranslationService.Translation> translations = translationService
|
|
||||||
.translateToAllLanguages(confirmationTask.getButtonText());
|
|
||||||
taskNode.set("buttonText", createTranslationArray(translations));
|
|
||||||
|
|
||||||
// Also translate buttonText in taskSpecificData if present
|
|
||||||
if (taskNode.has("taskSpecificData")) {
|
if (taskNode.has("taskSpecificData")) {
|
||||||
ObjectNode taskSpecificData = (ObjectNode) taskNode.get("taskSpecificData");
|
ObjectNode tsd = (ObjectNode) taskNode.get("taskSpecificData");
|
||||||
if (taskSpecificData.has("buttonText")) {
|
if (tsd.has("buttonText")) {
|
||||||
taskSpecificData.set("buttonText", createTranslationArray(translations));
|
tsd.set("buttonText", createTranslationArray(t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,17 +194,18 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate cargo item descriptions
|
// Cargo items
|
||||||
if (dto.getCargoItems() != null && !dto.getCargoItems().isEmpty()) {
|
if (dto.getCargoItems() != null && !dto.getCargoItems().isEmpty()) {
|
||||||
ArrayNode cargoItemsNode = root.withArray("cargoItems");
|
ArrayNode cargoItemsNode = root.withArray("cargoItems");
|
||||||
for (int i = 0; i < dto.getCargoItems().size(); i++) {
|
for (int i = 0; i < dto.getCargoItems().size(); i++) {
|
||||||
CargoItem item = dto.getCargoItems().get(i);
|
CargoItem item = dto.getCargoItems().get(i);
|
||||||
ObjectNode itemNode = (ObjectNode) cargoItemsNode.get(i);
|
ObjectNode itemNode = (ObjectNode) cargoItemsNode.get(i);
|
||||||
|
|
||||||
if (item.getDescription() != null && !item.getDescription().isBlank()) {
|
if (isNonBlank(item.getDescription())) {
|
||||||
List<TranslationService.Translation> translations = translationService
|
List<TranslationService.Translation> t = translations.get(item.getDescription());
|
||||||
.translateToAllLanguages(item.getDescription());
|
if (t != null) {
|
||||||
itemNode.set("description", createTranslationArray(translations));
|
itemNode.set("description", createTranslationArray(t));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,4 +226,8 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isNonBlank(String s) {
|
||||||
|
return s != null && !s.isBlank();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.assecutor.votianlt.service;
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import de.assecutor.votianlt.ai.service.LlmRestClient;
|
import de.assecutor.votianlt.ai.service.LlmRestClient;
|
||||||
import de.assecutor.votianlt.model.Language;
|
import de.assecutor.votianlt.model.Language;
|
||||||
import de.assecutor.votianlt.model.TranslationCacheEntry;
|
import de.assecutor.votianlt.model.TranslationCacheEntry;
|
||||||
@@ -20,6 +22,11 @@ import java.util.Optional;
|
|||||||
/**
|
/**
|
||||||
* Service for translating text into all available languages. Uses LLM API for
|
* Service for translating text into all available languages. Uses LLM API for
|
||||||
* translations with MongoDB-backed caching.
|
* translations with MongoDB-backed caching.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The central method is {@link #translateBatch(List)}: it checks the cache for
|
||||||
|
* every requested text and, for all cache misses, issues exactly ONE LLM call
|
||||||
|
* that returns translations for all missing texts and all languages at once.
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -30,10 +37,13 @@ public class TranslationService {
|
|||||||
|
|
||||||
private final LlmRestClient llmRestClient;
|
private final LlmRestClient llmRestClient;
|
||||||
private final TranslationCacheRepository cacheRepository;
|
private final TranslationCacheRepository cacheRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public TranslationService(LlmRestClient llmRestClient, TranslationCacheRepository cacheRepository) {
|
public TranslationService(LlmRestClient llmRestClient, TranslationCacheRepository cacheRepository,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
this.llmRestClient = llmRestClient;
|
this.llmRestClient = llmRestClient;
|
||||||
this.cacheRepository = cacheRepository;
|
this.cacheRepository = cacheRepository;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,8 +61,71 @@ public class TranslationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates the given text into all available languages. Uses MongoDB cache to
|
* Translates a list of texts into all available languages using at most one LLM
|
||||||
* avoid redundant LLM calls.
|
* call. Texts already present in the MongoDB cache are served from there;
|
||||||
|
* remaining cache misses are translated together in a single LLM request.
|
||||||
|
*
|
||||||
|
* @param texts
|
||||||
|
* the texts to translate (duplicates and blanks are ignored)
|
||||||
|
* @return map from each source text to its list of translations
|
||||||
|
*/
|
||||||
|
public Map<String, List<Translation>> translateBatch(List<String> texts) {
|
||||||
|
if (texts == null || texts.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<Translation>> result = new LinkedHashMap<>();
|
||||||
|
List<String> cacheMisses = new ArrayList<>();
|
||||||
|
|
||||||
|
// Phase 1: serve from cache where possible
|
||||||
|
for (String text : texts) {
|
||||||
|
if (text == null || text.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Optional<TranslationCacheEntry> cached = cacheRepository.findBySourceText(text);
|
||||||
|
if (cached.isPresent()) {
|
||||||
|
log.debug("[TranslationCache] Cache hit for '{}'", text);
|
||||||
|
result.put(text, toTranslationList(cached.get().getTranslations()));
|
||||||
|
} else {
|
||||||
|
cacheMisses.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheMisses.isEmpty()) {
|
||||||
|
log.debug("[TranslationCache] All {} texts served from cache", texts.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("[TranslationCache] {} cached, {} cache misses – calling LLM once", result.size(),
|
||||||
|
cacheMisses.size());
|
||||||
|
|
||||||
|
// Phase 2: one LLM call for all cache misses
|
||||||
|
Map<Integer, Map<String, String>> batchResult = callLlmForBatch(cacheMisses);
|
||||||
|
|
||||||
|
for (int i = 0; i < cacheMisses.size(); i++) {
|
||||||
|
String text = cacheMisses.get(i);
|
||||||
|
Map<String, String> translationMap = batchResult.getOrDefault(i, Map.of());
|
||||||
|
|
||||||
|
List<Translation> translations = buildTranslationList(text, translationMap);
|
||||||
|
result.put(text, translations);
|
||||||
|
|
||||||
|
// Persist in MongoDB cache
|
||||||
|
Map<String, String> mapToStore = translationMap.isEmpty() ? toTranslationMap(translations) : translationMap;
|
||||||
|
try {
|
||||||
|
cacheRepository.save(new TranslationCacheEntry(text, mapToStore));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[TranslationCache] Could not persist cache entry (may be duplicate): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("[TranslationCache] Batch complete – {} texts translated, cache size: {}", cacheMisses.size(),
|
||||||
|
cacheRepository.count());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper for a single text. Delegates to
|
||||||
|
* {@link #translateBatch(List)}.
|
||||||
*
|
*
|
||||||
* @param text
|
* @param text
|
||||||
* the text to translate
|
* the text to translate
|
||||||
@@ -62,37 +135,14 @@ public class TranslationService {
|
|||||||
if (text == null || text.isBlank()) {
|
if (text == null || text.isBlank()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
return translateBatch(List.of(text)).getOrDefault(text, List.of());
|
||||||
// Check MongoDB cache first
|
|
||||||
Optional<TranslationCacheEntry> cached = cacheRepository.findBySourceText(text);
|
|
||||||
if (cached.isPresent()) {
|
|
||||||
log.debug("[TranslationCache] Cache hit for '{}'", text);
|
|
||||||
return toTranslationList(cached.get().getTranslations());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache miss - translate via LLM
|
|
||||||
log.debug("[TranslationCache] Cache miss for '{}', calling LLM", text);
|
|
||||||
List<Translation> translations = translateViaLlm(text);
|
|
||||||
|
|
||||||
// Persist in MongoDB cache
|
|
||||||
Map<String, String> translationMap = toTranslationMap(translations);
|
|
||||||
try {
|
|
||||||
cacheRepository.save(new TranslationCacheEntry(text, translationMap));
|
|
||||||
log.debug("[TranslationCache] Stored translations for '{}', cache size: {}", text,
|
|
||||||
cacheRepository.count());
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Duplicate key on concurrent insert is harmless - log and continue
|
|
||||||
log.debug("[TranslationCache] Could not persist cache entry (may be duplicate): {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scheduled cleanup: if the cache exceeds 100,000 entries, deletes the oldest
|
* Scheduled cleanup: if the cache exceeds 100,000 entries, deletes the oldest
|
||||||
* entries until only 50,000 remain. Runs every hour.
|
* entries until only 50,000 remain. Runs every hour.
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedDelay = 3_600_000) // every hour
|
@Scheduled(fixedDelay = 3_600_000)
|
||||||
public void cleanupCacheIfNeeded() {
|
public void cleanupCacheIfNeeded() {
|
||||||
long count = cacheRepository.count();
|
long count = cacheRepository.count();
|
||||||
log.debug("[TranslationCache] Scheduled cleanup check – current size: {}", count);
|
log.debug("[TranslationCache] Scheduled cleanup check – current size: {}", count);
|
||||||
@@ -106,69 +156,96 @@ public class TranslationService {
|
|||||||
PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
||||||
|
|
||||||
cacheRepository.deleteAll(oldest);
|
cacheRepository.deleteAll(oldest);
|
||||||
log.info("[TranslationCache] Deleted {} entries, new size: {}", oldest.size(),
|
log.info("[TranslationCache] Deleted {} entries, new size: {}", oldest.size(), cacheRepository.count());
|
||||||
cacheRepository.count());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns cache statistics for monitoring.
|
* Returns the current number of entries in the MongoDB translation cache.
|
||||||
*/
|
*/
|
||||||
public long getCacheSize() {
|
public long getCacheSize() {
|
||||||
return cacheRepository.count();
|
return cacheRepository.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the translation cache.
|
* Clears the entire translation cache.
|
||||||
*/
|
*/
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
cacheRepository.deleteAll();
|
cacheRepository.deleteAll();
|
||||||
log.info("[TranslationCache] Cache cleared");
|
log.info("[TranslationCache] Cache cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- private helpers ---
|
// ---------------------------------------------------------------------------
|
||||||
|
// private helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private List<Translation> translateViaLlm(String text) {
|
/**
|
||||||
List<Translation> translations = new ArrayList<>();
|
* Sends a single LLM request for a list of texts and returns translations
|
||||||
for (Language lang : getAvailableLanguages()) {
|
* indexed by the position in the input list.
|
||||||
String translatedText = translateTextToLanguage(text, lang);
|
*/
|
||||||
translations.add(new Translation(lang.name().toLowerCase(), translatedText));
|
private Map<Integer, Map<String, String>> callLlmForBatch(List<String> texts) {
|
||||||
}
|
StringBuilder textsListing = new StringBuilder();
|
||||||
return translations;
|
for (int i = 0; i < texts.size(); i++) {
|
||||||
|
textsListing.append(i).append(": ").append(texts.get(i)).append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String translateTextToLanguage(String text, Language targetLanguage) {
|
String systemPrompt = """
|
||||||
String languageName = getLanguageName(targetLanguage);
|
You are a professional translator.
|
||||||
String systemPrompt = "You are a professional translator. Translate the given text to " + languageName
|
Translate ALL of the following numbered texts into ALL of these languages and return \
|
||||||
+ ". Return ONLY the translated text, nothing else. Do not add quotes or explanations.";
|
the result as a single valid JSON object.
|
||||||
|
Languages: de (German), en (English), fr (French), es (Spanish), tr (Turkish), \
|
||||||
|
pl (Polish), ru (Russian), ee (Estonian), lv (Latvian), lt (Lithuanian).
|
||||||
|
The JSON must use the text index as the key (as a string: "0", "1", ...) and an \
|
||||||
|
object with language codes as the value.
|
||||||
|
Return ONLY the JSON object, no markdown, no explanations, no extra text.
|
||||||
|
Example for 2 texts:
|
||||||
|
{"0":{"de":"...","en":"...","fr":"...","es":"...","tr":"...","pl":"...","ru":"...","ee":"...","lv":"...","lt":"..."},\
|
||||||
|
"1":{"de":"...","en":"...","fr":"...","es":"...","tr":"...","pl":"...","ru":"...","ee":"...","lv":"...","lt":"..."}}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Estimate output tokens: ~400 tokens per text × languages, capped at 8 000
|
||||||
|
int maxTokens = Math.min(500 + 400 * texts.size(), 8_000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String result = llmRestClient.chat(systemPrompt, text, 0.3, 500);
|
String raw = llmRestClient.chat(systemPrompt, textsListing.toString(), 0.3, maxTokens);
|
||||||
if (result != null && !result.isBlank()) {
|
if (raw != null && !raw.isBlank()) {
|
||||||
result = result.trim();
|
raw = extractJsonObject(raw);
|
||||||
if (result.startsWith("\"") && result.endsWith("\"")) {
|
Map<String, Map<String, String>> parsed = objectMapper.readValue(raw, new TypeReference<>() {
|
||||||
result = result.substring(1, result.length() - 1);
|
});
|
||||||
|
Map<Integer, Map<String, String>> indexed = new LinkedHashMap<>();
|
||||||
|
parsed.forEach((key, value) -> {
|
||||||
|
try {
|
||||||
|
indexed.put(Integer.parseInt(key), value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("[TranslationCache] Unexpected key in batch LLM response: {}", key);
|
||||||
}
|
}
|
||||||
return result.trim();
|
});
|
||||||
|
return indexed;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Translation failed for {}: {}", languageName, e.getMessage());
|
log.warn("[TranslationCache] Batch LLM call failed: {}", e.getMessage());
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getLanguageName(Language language) {
|
return Map.of();
|
||||||
return switch (language) {
|
}
|
||||||
case DE -> "German";
|
|
||||||
case EN -> "English";
|
private List<Translation> buildTranslationList(String fallback, Map<String, String> translationMap) {
|
||||||
case FR -> "French";
|
List<Translation> list = new ArrayList<>();
|
||||||
case ES -> "Spanish";
|
for (Language lang : getAvailableLanguages()) {
|
||||||
case TR -> "Turkish";
|
String code = lang.name().toLowerCase();
|
||||||
case PL -> "Polish";
|
String translated = translationMap.getOrDefault(code, fallback);
|
||||||
case RU -> "Russian";
|
list.add(new Translation(code, (translated == null || translated.isBlank()) ? fallback : translated));
|
||||||
case EE -> "Estonian";
|
}
|
||||||
case LV -> "Latvian";
|
return list;
|
||||||
case LT -> "Lithuanian";
|
}
|
||||||
};
|
|
||||||
|
private static String extractJsonObject(String raw) {
|
||||||
|
int start = raw.indexOf('{');
|
||||||
|
int end = raw.lastIndexOf('}');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
return raw.substring(start, end + 1);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, String> toTranslationMap(List<Translation> translations) {
|
private static Map<String, String> toTranslationMap(List<Translation> translations) {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
# LLM Configuration (LM Studio)
|
# LLM Configuration (LM Studio)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
spring.ai.openai.base-url=http://192.168.180.3:1234
|
spring.ai.openai.base-url=https://lmstudio.appcreation.de
|
||||||
spring.ai.openai.api-key=not-used
|
spring.ai.openai.api-key=not-used
|
||||||
spring.ai.openai.chat.options.model=local-model
|
spring.ai.openai.chat.options.model=local-model
|
||||||
spring.ai.openai.chat.options.temperature=0.7
|
spring.ai.openai.chat.options.temperature=0.7
|
||||||
|
|||||||
Reference in New Issue
Block a user