diff --git a/pom.xml b/pom.xml index 35267b4..b1f49d6 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ com.google.genai google-genai - 1.8.0 + 1.13.0 com.google.protobuf diff --git a/src/main/java/com/example/dto/dialogflow/notification/ExternalNotRequestDTO.java b/src/main/java/com/example/dto/dialogflow/notification/ExternalNotRequestDTO.java index 3113deb..ae1cae7 100644 --- a/src/main/java/com/example/dto/dialogflow/notification/ExternalNotRequestDTO.java +++ b/src/main/java/com/example/dto/dialogflow/notification/ExternalNotRequestDTO.java @@ -9,8 +9,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public record ExternalNotRequestDTO( - @JsonProperty("texto") String text, - @JsonProperty("telefono") String phoneNumber) { + @JsonProperty("texto") String text, + @JsonProperty("telefono") String phoneNumber, + @JsonProperty("parametrosOcultos") java.util.Map hiddenParameters +) { public ExternalNotRequestDTO { } } \ No newline at end of file diff --git a/src/main/java/com/example/dto/quickreplies/QuickReplyDTO.java b/src/main/java/com/example/dto/quickreplies/QuickReplyDTO.java index 9f690ef..cf1b97c 100644 --- a/src/main/java/com/example/dto/quickreplies/QuickReplyDTO.java +++ b/src/main/java/com/example/dto/quickreplies/QuickReplyDTO.java @@ -3,5 +3,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; public record QuickReplyDTO( @JsonProperty("header") String header, + @JsonProperty("body") String body, + @JsonProperty("button") String button, + @JsonProperty("header_section") String headerSection, @JsonProperty("preguntas") List preguntas ) {} \ No newline at end of file diff --git a/src/main/java/com/example/exception/DialogflowClientException.java b/src/main/java/com/example/exception/DialogflowClientException.java index a30a665..47badd5 100644 --- a/src/main/java/com/example/exception/DialogflowClientException.java +++ b/src/main/java/com/example/exception/DialogflowClientException.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose. + * Your use of it is subject to your agreement with Google. + */ + package com.example.exception; public class DialogflowClientException extends RuntimeException { diff --git a/src/main/java/com/example/exception/FirestorePersistenceException.java b/src/main/java/com/example/exception/FirestorePersistenceException.java index 28a3d17..b53abcc 100644 --- a/src/main/java/com/example/exception/FirestorePersistenceException.java +++ b/src/main/java/com/example/exception/FirestorePersistenceException.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose. + * Your use of it is subject to your agreement with Google. + */ + package com.example.exception; public class FirestorePersistenceException extends RuntimeException { diff --git a/src/main/java/com/example/exception/GeminiClientException.java b/src/main/java/com/example/exception/GeminiClientException.java index 6e76ee2..fed2b7b 100644 --- a/src/main/java/com/example/exception/GeminiClientException.java +++ b/src/main/java/com/example/exception/GeminiClientException.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose. + * Your use of it is subject to your agreement with Google. + */ + package com.example.exception; public class GeminiClientException extends Exception { diff --git a/src/main/java/com/example/service/conversation/ConversationManagerService.java b/src/main/java/com/example/service/conversation/ConversationManagerService.java index a563e8b..91e492a 100644 --- a/src/main/java/com/example/service/conversation/ConversationManagerService.java +++ b/src/main/java/com/example/service/conversation/ConversationManagerService.java @@ -47,7 +47,6 @@ public class ConversationManagerService { private final ConversationContextMapper conversationContextMapper; private final DataLossPrevention dataLossPrevention; private final String dlpTemplateCompleteFlow; - private final String dlpTemplatePersistFlow; public ConversationManagerService( DialogflowClientService dialogflowServiceClient, @@ -60,8 +59,7 @@ public class ConversationManagerService { NotificationContextMapper notificationContextMapper, ConversationContextMapper conversationContextMapper, DataLossPrevention dataLossPrevention, - @Value("${google.cloud.dlp.dlpTemplateCompleteFlow}") String dlpTemplateCompleteFlow, - @Value("${google.cloud.dlp.dlpTemplatePersistFlow}") String dlpTemplatePersistFlow) { + @Value("${google.cloud.dlp.dlpTemplateCompleteFlow}") String dlpTemplateCompleteFlow) { this.dialogflowServiceClient = dialogflowServiceClient; this.firestoreConversationService = firestoreConversationService; this.memoryStoreConversationService = memoryStoreConversationService; @@ -73,7 +71,6 @@ public class ConversationManagerService { this.conversationContextMapper = conversationContextMapper; this.dataLossPrevention = dataLossPrevention; this.dlpTemplateCompleteFlow = dlpTemplateCompleteFlow; - this.dlpTemplatePersistFlow = dlpTemplatePersistFlow; } public Mono manageConversation(ExternalConvRequestDTO externalrequest) { return dataLossPrevention.getObfuscatedString(externalrequest.message(), dlpTemplateCompleteFlow) @@ -193,10 +190,7 @@ public class ConversationManagerService { private Mono processDialogflowRequest(ConversationSessionDTO session, DetectIntentRequestDTO request, String userId, String userMessageText, String userPhoneNumber, boolean newSession) { final String finalSessionId = session.sessionId(); - return dataLossPrevention.getObfuscatedString(userMessageText, dlpTemplatePersistFlow) - .flatMap(obfuscatedUserMessageText -> { - - ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(obfuscatedUserMessageText); + ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText); return this.persistConversationTurn(userId, finalSessionId, userEntry, userPhoneNumber) .doOnSuccess(v -> logger.debug("User entry successfully persisted for session {}. Proceeding to Dialogflow...", finalSessionId)) @@ -210,7 +204,6 @@ public class ConversationManagerService { }) .doOnError(error -> logger.error("Overall error during conversation management for session {}: {}", finalSessionId, error.getMessage(), error)) )); - }); } private Mono startNotificationConversation(ConversationContext context, DetectIntentRequestDTO request, NotificationDTO notification) { final String userId = context.userId(); diff --git a/src/main/java/com/example/service/notification/MemoryStoreNotificationService.java b/src/main/java/com/example/service/notification/MemoryStoreNotificationService.java index 611412b..e90b0b7 100644 --- a/src/main/java/com/example/service/notification/MemoryStoreNotificationService.java +++ b/src/main/java/com/example/service/notification/MemoryStoreNotificationService.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose. + * Your use of it is subject to your agreement with Google. + */ + package com.example.service.notification; import com.example.dto.dialogflow.conversation.ConversationEntryDTO; diff --git a/src/main/java/com/example/service/notification/NotificationManagerService.java b/src/main/java/com/example/service/notification/NotificationManagerService.java index 92ddf31..c284a2c 100644 --- a/src/main/java/com/example/service/notification/NotificationManagerService.java +++ b/src/main/java/com/example/service/notification/NotificationManagerService.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose. + * Your use of it is subject to your agreement with Google. + */ + package com.example.service.notification; import com.example.dto.dialogflow.notification.ExternalNotRequestDTO; @@ -103,7 +108,11 @@ public Mono processNotification(ExternalNotRequestDTO e parameters.put("telefono", telefono); parameters.put(NOTIFICATION_TEXT_PARAM, newNotificationEntry.texto()); - // Use a TextInputDTO to correctly build the QueryInputDTO + if (externalRequest.hiddenParameters() != null && !externalRequest.hiddenParameters().isEmpty()) { + parameters.putAll(externalRequest.hiddenParameters()); + } + + // Use a TextInputDTO to correctly build the QueryInputDTO TextInputDTO textInput = new TextInputDTO(newNotificationEntry.texto()); QueryInputDTO queryInput = new QueryInputDTO(textInput, null, defaultLanguageCode); diff --git a/src/main/java/com/example/service/quickreplies/QuickRepliesManagerService.java b/src/main/java/com/example/service/quickreplies/QuickRepliesManagerService.java index adb42b8..db7b773 100644 --- a/src/main/java/com/example/service/quickreplies/QuickRepliesManagerService.java +++ b/src/main/java/com/example/service/quickreplies/QuickRepliesManagerService.java @@ -11,7 +11,6 @@ import com.example.dto.dialogflow.conversation.ConversationEntryEntity; import com.example.dto.dialogflow.conversation.ConversationEntryType; import com.example.dto.dialogflow.conversation.ExternalConvRequestDTO; import com.example.dto.quickreplies.QuickReplyScreenRequestDTO; -import com.example.service.conversation.DataLossPrevention; import com.example.dto.quickreplies.QuestionDTO; import com.example.dto.quickreplies.QuickReplyDTO; import com.example.service.conversation.FirestoreConversationService; @@ -20,10 +19,14 @@ import com.example.util.SessionIdGenerator; import java.time.Instant; import java.util.Collections; import java.util.List; +import java.util.stream.IntStream; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import com.example.dto.dialogflow.conversation.QueryResultDTO; +import com.example.service.conversation.ConversationManagerService; +import org.springframework.context.annotation.Lazy; import reactor.core.publisher.Mono; @Service @@ -32,20 +35,17 @@ public class QuickRepliesManagerService { private final MemoryStoreConversationService memoryStoreConversationService; private final FirestoreConversationService firestoreConversationService; private final QuickReplyContentService quickReplyContentService; - private final DataLossPrevention dataLossPrevention; - private final String dlpTemplatePersistFlow; + private final ConversationManagerService conversationManagerService; public QuickRepliesManagerService( + @Lazy ConversationManagerService conversationManagerService, MemoryStoreConversationService memoryStoreConversationService, FirestoreConversationService firestoreConversationService, - QuickReplyContentService quickReplyContentService, - DataLossPrevention dataLossPrevention, - @Value("${google.cloud.dlp.dlpTemplatePersistFlow}") String dlpTemplatePersistFlow) { + QuickReplyContentService quickReplyContentService) { + this.conversationManagerService = conversationManagerService; this.memoryStoreConversationService = memoryStoreConversationService; this.firestoreConversationService = firestoreConversationService; this.quickReplyContentService = quickReplyContentService; - this.dataLossPrevention = dataLossPrevention; - this.dlpTemplatePersistFlow = dlpTemplatePersistFlow; } public Mono startQuickReplySession(QuickReplyScreenRequestDTO externalRequest) { @@ -88,13 +88,27 @@ public class QuickRepliesManagerService { .flatMap(session -> { String userId = session.userId(); String sessionId = session.sessionId(); - return dataLossPrevention.getObfuscatedString(externalRequest.message(), dlpTemplatePersistFlow) - .flatMap(obfuscatedMessage -> { - ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(obfuscatedMessage); - long userMessagesCount = session.entries().stream() - .filter(e -> e.entity() == ConversationEntryEntity.USUARIO) - .count(); + ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(externalRequest.message()); + + List entries = session.entries(); + int lastInitIndex = IntStream.range(0, entries.size()) + .map(i -> entries.size() - 1 - i) + .filter(i -> { + ConversationEntryDTO entry = entries.get(i); + return entry.entity() == ConversationEntryEntity.SISTEMA && entry.type() == ConversationEntryType.INICIO; + }) + .findFirst() + .orElse(-1); + + long userMessagesCount; + if (lastInitIndex != -1) { + userMessagesCount = entries.subList(lastInitIndex + 1, entries.size()).stream() + .filter(e -> e.entity() == ConversationEntryEntity.USUARIO) + .count(); + } else { + userMessagesCount = 0; + } if (userMessagesCount == 0) { // Is the first user message in the Quick-Replies flow // This is the second message of the flow. Return the full list. @@ -118,33 +132,32 @@ public class QuickRepliesManagerService { .filter(p -> p.titulo().equalsIgnoreCase(externalRequest.message().trim())) .toList(); - QuickReplyDTO responseQuickReplyDTO; if (!matchedPreguntas.isEmpty()) { - responseQuickReplyDTO = new QuickReplyDTO(quickReplyDTO.header(), - matchedPreguntas); - } else { - responseQuickReplyDTO = new QuickReplyDTO(quickReplyDTO.header(), - Collections.emptyList()); - } + // Matched question, return the answer + String respuesta = matchedPreguntas.get(0).respuesta(); + QueryResultDTO queryResult = new QueryResultDTO(respuesta, null); + DetectIntentResponseDTO response = new DetectIntentResponseDTO(sessionId, queryResult, null); - // End the quick reply flow by clearing the pantallaContexto - return memoryStoreConversationService + return memoryStoreConversationService .updateSession(session.withPantallaContexto(null)) .then(persistConversationTurn(userId, sessionId, - ConversationEntryDTO.forAgentWithMessage( - responseQuickReplyDTO.toString()), + ConversationEntryDTO.forAgentWithMessage(respuesta), userPhoneNumber, null)) - .thenReturn(new DetectIntentResponseDTO(sessionId, null, - responseQuickReplyDTO)); + .thenReturn(response); + } else { + // No match, delegate to Dialogflow + return memoryStoreConversationService + .updateSession(session.withPantallaContexto(null)) + .then(conversationManagerService.manageConversation(externalRequest)); + } }); } else { // Should not happen. End the flow. return memoryStoreConversationService.updateSession(session.withPantallaContexto(null)) .then(Mono.just(new DetectIntentResponseDTO(session.sessionId(), null, - new QuickReplyDTO("Flow Error", Collections.emptyList())))); + new QuickReplyDTO("Flow Error", null, null, null, Collections.emptyList())))); } }); - }); } private Mono persistConversationTurn(String userId, String sessionId, ConversationEntryDTO entry, diff --git a/src/main/java/com/example/service/quickreplies/QuickReplyContentService.java b/src/main/java/com/example/service/quickreplies/QuickReplyContentService.java index 9da8e9a..b785587 100644 --- a/src/main/java/com/example/service/quickreplies/QuickReplyContentService.java +++ b/src/main/java/com/example/service/quickreplies/QuickReplyContentService.java @@ -28,7 +28,7 @@ public class QuickReplyContentService { logger.info("Fetching quick replies from Firestore for document: {}", collectionId); if (collectionId == null || collectionId.isBlank()) { logger.warn("collectionId is null or empty. Returning empty quick replies."); - return Mono.just(new QuickReplyDTO("empty", Collections.emptyList())); + return Mono.just(new QuickReplyDTO("empty", null, null, null, Collections.emptyList())); } return Mono.fromCallable(() -> { try { @@ -45,11 +45,14 @@ public class QuickReplyContentService { .filter(DocumentSnapshot::exists) .map(document -> { String header = document.getString("header"); + String body = document.getString("body"); + String button = document.getString("button"); + String headerSection = document.getString("header_section"); List> preguntasData = (List>) document.get("preguntas"); List preguntas = preguntasData.stream() .map(p -> new QuestionDTO((String) p.get("titulo"), (String) p.get("descripcion"), (String) p.get("respuesta"))) .toList(); - return new QuickReplyDTO(header, preguntas); + return new QuickReplyDTO(header, body, button, headerSection, preguntas); }) .doOnSuccess(quickReplyDTO -> { if (quickReplyDTO != null) { diff --git a/src/main/java/com/example/util/FirestoreDataImporter.java b/src/main/java/com/example/util/FirestoreDataImporter.java index 185b13f..285cd65 100644 --- a/src/main/java/com/example/util/FirestoreDataImporter.java +++ b/src/main/java/com/example/util/FirestoreDataImporter.java @@ -9,15 +9,18 @@ import com.example.repository.FirestoreBaseRepository; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.DocumentSnapshot; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import java.io.IOException; import java.io.InputStream; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ExecutionException; @Component @@ -32,12 +35,17 @@ public class FirestoreDataImporter { @Autowired private ObjectMapper objectMapper; + @Autowired + private Environment env; + @PostConstruct public void importDataOnStartup() { - try { - importQuickReplies(); - } catch (Exception e) { - logger.error("Failed to import data to Firestore on startup", e); + if (Boolean.parseBoolean(env.getProperty("firestore.data.importer.enabled"))) { + try { + importQuickReplies(); + } catch (Exception e) { + logger.error("Failed to import data to Firestore on startup", e); + } } } @@ -67,13 +75,19 @@ public class FirestoreDataImporter { logger.warn("Resource not found: {}", resourcePath); return; } - Map data = objectMapper.readValue(inputStream, new TypeReference>() {}); + Map localData = objectMapper.readValue(inputStream, new TypeReference>() {}); DocumentReference docRef = firestoreBaseRepository.getDocumentReference(collectionPath, documentId); - if (!firestoreBaseRepository.documentExists(docRef)) { - firestoreBaseRepository.setDocument(docRef, data); - logger.debug("Successfully imported {} to Firestore.", documentId); + + if (firestoreBaseRepository.documentExists(docRef)) { + DocumentSnapshot documentSnapshot = firestoreBaseRepository.getDocumentSnapshot(docRef); + Map firestoreData = documentSnapshot.getData(); + if (!Objects.equals(localData, firestoreData)) { + firestoreBaseRepository.setDocument(docRef, localData); + logger.info("Successfully updated {} in Firestore.", documentId); + } } else { - logger.debug("{} already exists in Firestore. Skipping import.", documentId); + firestoreBaseRepository.setDocument(docRef, localData); + logger.info("Successfully imported {} to Firestore.", documentId); } } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 30cd2b6..e1fc4b0 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -64,4 +64,7 @@ messagefilter.prompt=prompts/message_filter_prompt.txt # (DLP) Configuration # ========================================================= google.cloud.dlp.dlpTemplateCompleteFlow=${DLP_TEMPLATE_COMPLETE_FLOW} -google.cloud.dlp.dlpTemplatePersistFlow=IMC_INSPECT_NAME \ No newline at end of file +# ========================================================= +# Quick-replies Preset-data +# ========================================================= +firestore.data.importer.enabled=true \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 937530a..a606aa8 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -58,3 +58,12 @@ messagefilter.temperature=${MESSAGE_FILTER_TEMPERATURE} messagefilter.maxOutputTokens=${MESSAGE_FILTER_MAX_OUTPUT_TOKENS} messagefilter.topP=${MESSAGE_FILTER_TOP_P} messagefilter.prompt=prompts/message_filter_prompt.txt +# ========================================================= +# (DLP) Configuration +# ========================================================= +google.cloud.dlp.dlpTemplateCompleteFlow=${DLP_TEMPLATE_COMPLETE_FLOW} +google.cloud.dlp.dlpTemplatePersistFlow=${DLP_TEMPLATE_PERSIST_FLOW} +# ========================================================= +# Quick-replies Preset-data +# ========================================================= +firestore.data.importer.enabled=true \ No newline at end of file diff --git a/src/main/resources/application-qa.properties b/src/main/resources/application-qa.properties index 937530a..a606aa8 100644 --- a/src/main/resources/application-qa.properties +++ b/src/main/resources/application-qa.properties @@ -58,3 +58,12 @@ messagefilter.temperature=${MESSAGE_FILTER_TEMPERATURE} messagefilter.maxOutputTokens=${MESSAGE_FILTER_MAX_OUTPUT_TOKENS} messagefilter.topP=${MESSAGE_FILTER_TOP_P} messagefilter.prompt=prompts/message_filter_prompt.txt +# ========================================================= +# (DLP) Configuration +# ========================================================= +google.cloud.dlp.dlpTemplateCompleteFlow=${DLP_TEMPLATE_COMPLETE_FLOW} +google.cloud.dlp.dlpTemplatePersistFlow=${DLP_TEMPLATE_PERSIST_FLOW} +# ========================================================= +# Quick-replies Preset-data +# ========================================================= +firestore.data.importer.enabled=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9b000d5..257b306 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1 @@ -spring.profiles.active=${SPRING_PROFILE} +spring.profiles.active=dev \ No newline at end of file diff --git a/src/main/resources/quick-replies/pagos.json b/src/main/resources/quick-replies/pagos.json index c9dd901..0db7034 100644 --- a/src/main/resources/quick-replies/pagos.json +++ b/src/main/resources/quick-replies/pagos.json @@ -1,5 +1,8 @@ { - "header": "preguntas sobre pagos", + "header": "preguntas frecuentes", + "body": "AquĆ­ tienes las preguntas frecuentes que suelen hacernos algunos de nuestros clientes", + "button": "Ver", + "header_section": "preguntas sobre pagos", "preguntas": [ { "titulo": "Donde veo mi historial de pagos?", diff --git a/src/test/java/com/example/service/unit_testing/QuickReplyContentServiceTest.java b/src/test/java/com/example/service/unit_testing/QuickReplyContentServiceTest.java index e12b9ee..0d48e00 100644 --- a/src/test/java/com/example/service/unit_testing/QuickReplyContentServiceTest.java +++ b/src/test/java/com/example/service/unit_testing/QuickReplyContentServiceTest.java @@ -54,13 +54,16 @@ public class QuickReplyContentServiceTest { // Given String collectionId = "home"; String header = "home_header"; + String body = "home_body"; + String button = "home_button"; + String headerSection = "home_header_section"; List> preguntas = Collections.singletonList( Map.of("titulo", "title", "descripcion", "description", "respuesta", "response") ); List questionDTOs = Collections.singletonList( new QuestionDTO("title", "description", "response") ); - QuickReplyDTO expected = new QuickReplyDTO(header, questionDTOs); + QuickReplyDTO expected = new QuickReplyDTO(header, body, button, headerSection, questionDTOs); when(firestore.collection("artifacts")).thenReturn(collectionReference); when(collectionReference.document("default-app-id")).thenReturn(documentReference); @@ -70,6 +73,9 @@ public class QuickReplyContentServiceTest { when(apiFuture.get()).thenReturn(documentSnapshot); when(documentSnapshot.exists()).thenReturn(true); when(documentSnapshot.getString("header")).thenReturn(header); + when(documentSnapshot.getString("body")).thenReturn(body); + when(documentSnapshot.getString("button")).thenReturn(button); + when(documentSnapshot.getString("header_section")).thenReturn(headerSection); when(documentSnapshot.get("preguntas")).thenReturn(preguntas); // When @@ -112,7 +118,7 @@ public class QuickReplyContentServiceTest { // Then StepVerifier.create(result) - .expectNext(new QuickReplyDTO("empty", Collections.emptyList())) + .expectNext(new QuickReplyDTO("empty", null, null, null, Collections.emptyList())) .verifyComplete(); } }