Prise en compte du langage de l'utilisateur pour le prompt de réponse. Si par exemple l'interface est en anglais, les IA vont favoriser l'anglais pour la réponse
This commit is contained in:
@@ -10,6 +10,9 @@ import java.util.List;
|
||||
*/
|
||||
public interface ConversationTitleGenerator {
|
||||
|
||||
/** Renvoie un titre en francais (4-7 mots max). Jamais null ni vide. */
|
||||
/**
|
||||
* Renvoie un titre court (4-7 mots max), dans la langue de l'utilisateur
|
||||
* (relayee au Brain via l'entete X-User-Language). Jamais null ni vide.
|
||||
*/
|
||||
String generate(List<ConversationMessage> firstMessages);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.loremind.domain.generationcontext.ChatRequest;
|
||||
import com.loremind.domain.generationcontext.ChatUsage;
|
||||
import com.loremind.domain.generationcontext.ports.AiChatProvider;
|
||||
import com.loremind.domain.generationcontext.ports.AiProviderException;
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -64,6 +65,7 @@ public class BrainAiChatClient implements AiChatProvider {
|
||||
|
||||
Flux<ServerSentEvent<String>> flux = webClient.post()
|
||||
.uri(CHAT_STREAM_PATH)
|
||||
.header(UserLanguageHolder.HEADER, UserLanguageHolder.get())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.bodyValue(payload)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.loremind.infrastructure.ai;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.loremind.domain.campaigncontext.ports.CampaignPdfAdvisor;
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
@@ -65,6 +66,7 @@ public class BrainCampaignAdaptClient implements CampaignPdfAdvisor {
|
||||
|
||||
Flux<ServerSentEvent<String>> flux = webClient.post()
|
||||
.uri(ADAPT_PATH)
|
||||
.header(UserLanguageHolder.HEADER, UserLanguageHolder.get())
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.body(BodyInserters.fromMultipartData(parts.build()))
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.loremind.domain.campaigncontext.CampaignImportProposal.RoomProposal;
|
||||
import com.loremind.domain.campaigncontext.CampaignImportProposal.SceneProposal;
|
||||
import com.loremind.domain.campaigncontext.ports.CampaignImportException;
|
||||
import com.loremind.domain.campaigncontext.ports.CampaignPdfImporter;
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
@@ -71,6 +72,7 @@ public class BrainCampaignImportClient implements CampaignPdfImporter {
|
||||
|
||||
Flux<ServerSentEvent<String>> flux = webClient.post()
|
||||
.uri(IMPORT_CAMPAIGN_STREAM_PATH)
|
||||
.header(UserLanguageHolder.HEADER, UserLanguageHolder.get())
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.body(BodyInserters.fromMultipartData(parts.build()))
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.loremind.infrastructure.ai;
|
||||
|
||||
import com.loremind.domain.conversationcontext.ConversationMessage;
|
||||
import com.loremind.domain.conversationcontext.ports.ConversationTitleGenerator;
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -50,6 +51,7 @@ public class BrainConversationTitleClient implements ConversationTitleGenerator
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> resp = webClient.post()
|
||||
.uri(PATH)
|
||||
.header(UserLanguageHolder.HEADER, UserLanguageHolder.get())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(payload)
|
||||
.retrieve()
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.loremind.domain.campaigncontext.ports.NotebookChatStreamer;
|
||||
import com.loremind.domain.campaigncontext.ports.NotebookException;
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -66,6 +67,7 @@ public class BrainNotebookChatClient implements NotebookChatStreamer {
|
||||
|
||||
Flux<ServerSentEvent<String>> flux = webClient.post()
|
||||
.uri(deep ? DEEP_PATH : PATH)
|
||||
.header(UserLanguageHolder.HEADER, UserLanguageHolder.get())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.bodyValue(payload)
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.loremind.domain.gamesystemcontext.RulesImportProgress;
|
||||
import com.loremind.domain.gamesystemcontext.RulesImportResult;
|
||||
import com.loremind.domain.gamesystemcontext.ports.RulesImportException;
|
||||
import com.loremind.domain.gamesystemcontext.ports.RulesPdfImporter;
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
@@ -125,6 +126,7 @@ public class BrainRulesImportClient implements RulesPdfImporter {
|
||||
|
||||
Flux<ServerSentEvent<String>> flux = webClient.post()
|
||||
.uri(IMPORT_RULES_STREAM_PATH)
|
||||
.header(UserLanguageHolder.HEADER, UserLanguageHolder.get())
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.body(BodyInserters.fromMultipartData(parts.build()))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.loremind.infrastructure.ai;
|
||||
|
||||
import com.loremind.infrastructure.web.config.UserLanguageHolder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
@@ -17,6 +18,12 @@ import java.time.Duration;
|
||||
* <p>
|
||||
* Sans cette entete, le Brain refuse la requete (401) — defense contre
|
||||
* l'acces direct au Brain depuis un attaquant qui atteindrait son port.
|
||||
* <p>
|
||||
* Relaie aussi l'entete X-User-Language (langue choisie dans l'UI, capturee par
|
||||
* {@link com.loremind.infrastructure.web.config.UserLanguageFilter}) pour que le
|
||||
* Brain redige ses reponses IA dans la langue de l'utilisateur. Lu depuis le
|
||||
* ThreadLocal au moment de l'execution de la requete (thread servlet) — d'ou
|
||||
* l'usage d'un interceptor (et non d'un defaultHeader fige au demarrage).
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
@@ -36,6 +43,7 @@ public class RestTemplateConfig {
|
||||
if (internalSecret != null && !internalSecret.isBlank()) {
|
||||
request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret);
|
||||
}
|
||||
request.getHeaders().set(UserLanguageHolder.HEADER, UserLanguageHolder.get());
|
||||
return execution.execute(request, body);
|
||||
})
|
||||
.build();
|
||||
@@ -59,6 +67,7 @@ public class RestTemplateConfig {
|
||||
if (internalSecret != null && !internalSecret.isBlank()) {
|
||||
request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret);
|
||||
}
|
||||
request.getHeaders().set(UserLanguageHolder.HEADER, UserLanguageHolder.get());
|
||||
return execution.execute(request, body);
|
||||
})
|
||||
.build();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.loremind.infrastructure.web.config;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Capture la langue de l'utilisateur (entête {@code X-User-Language} envoyé par le
|
||||
* frontend) dans {@link UserLanguageHolder} pour la durée de la requête, puis la
|
||||
* nettoie systématiquement.
|
||||
* <p>
|
||||
* Les clients du Brain liront ce ThreadLocal au moment de construire leur appel
|
||||
* (sur ce même thread servlet) pour relayer la langue au Brain. Indispensable de
|
||||
* {@code clear()} en {@code finally} : les threads servlet sont recyclés dans un
|
||||
* pool, une valeur oubliée fuiterait sur la requête suivante.
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class UserLanguageFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
try {
|
||||
UserLanguageHolder.set(request.getHeader(UserLanguageHolder.HEADER));
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
UserLanguageHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.loremind.infrastructure.web.config;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Langue de l'utilisateur courant, portée par un ThreadLocal le temps d'une
|
||||
* requête HTTP entrante.
|
||||
* <p>
|
||||
* Le frontend Angular envoie son choix de langue (code court {@code fr}/{@code en})
|
||||
* via l'entête {@code X-User-Language}. {@link UserLanguageFilter} la capture ici,
|
||||
* et les clients du Brain ({@code RestTemplateConfig} pour les appels bloquants,
|
||||
* les clients WebClient pour le streaming) la relaient au Brain — qui rédige alors
|
||||
* ses réponses IA dans cette langue.
|
||||
* <p>
|
||||
* Repli systématique sur le français si rien n'est fourni (vieux client, appel interne).
|
||||
*/
|
||||
public final class UserLanguageHolder {
|
||||
|
||||
/** Nom de l'entête HTTP relayant la langue, du frontend jusqu'au Brain. */
|
||||
public static final String HEADER = "X-User-Language";
|
||||
|
||||
/** Langue par défaut quand l'entête est absent ou non reconnu. */
|
||||
public static final String DEFAULT = "fr";
|
||||
|
||||
/** Langues supportées (alignées sur LanguageService Angular et NAMES côté Brain). */
|
||||
private static final Set<String> SUPPORTED = Set.of("fr", "en");
|
||||
|
||||
private static final ThreadLocal<String> CURRENT = ThreadLocal.withInitial(() -> DEFAULT);
|
||||
|
||||
private UserLanguageHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise un code/entête langue arbitraire vers un code supporté.
|
||||
* Tolère la casse, les variantes régionales ({@code en-US}) et un
|
||||
* {@code Accept-Language} complet ({@code fr-FR,fr;q=0.9}). Repli {@code DEFAULT}.
|
||||
*/
|
||||
public static String normalize(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return DEFAULT;
|
||||
}
|
||||
String primary = raw.split(",")[0].split(";")[0].trim().toLowerCase();
|
||||
String base = primary.split("-")[0];
|
||||
return SUPPORTED.contains(base) ? base : DEFAULT;
|
||||
}
|
||||
|
||||
public static void set(String language) {
|
||||
CURRENT.set(normalize(language));
|
||||
}
|
||||
|
||||
public static String get() {
|
||||
return CURRENT.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CURRENT.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user