UPDATE int-layer 25-Ago

This commit is contained in:
PAVEL PALMA
2025-08-26 00:43:44 -06:00
parent 404f152097
commit 2a72bf9550
27 changed files with 785 additions and 336 deletions

View File

@@ -119,6 +119,10 @@
<groupId>com.google.api</groupId>
<artifactId>gax</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-dlp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

BIN
src/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,15 @@
package com.example.config;
import com.google.cloud.dlp.v2.DlpServiceClient;
import java.io.IOException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DlpConfig {
@Bean(destroyMethod = "close")
public DlpServiceClient dlpServiceClient() throws IOException {
return DlpServiceClient.create();
}
}

View File

@@ -31,6 +31,7 @@ public class ConversationSummaryController {
this.conversationSummaryService = conversationSummaryService;
}
@PostMapping("/conversation")
public ResponseEntity<ConversationSummaryResponse> summarizeConversation(
@Valid @RequestBody ConversationSummaryRequest request) {

View File

@@ -5,6 +5,8 @@
package com.example.dto.dialogflow.conversation;
import com.example.dto.dialogflow.notification.EventInputDTO;
public record QueryInputDTO(

View File

@@ -1,8 +1,7 @@
package com.example.dto.quickreplies;
import com.fasterxml.jackson.annotation.JsonProperty;
public record QuestionDTO(
@JsonProperty("titulo") String titulo,
@JsonProperty("descripcion") String descripcion
@JsonProperty("descripcion") String descripcion,
@JsonProperty("respuesta") String respuesta
) {}

View File

@@ -1,8 +1,6 @@
package com.example.dto.quickreplies;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record QuickReplyDTO(
@JsonProperty("header") String header,
@JsonProperty("preguntas") List<QuestionDTO> preguntas

View File

@@ -8,8 +8,9 @@ package com.example.mapper.conversation;
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
import com.example.dto.dialogflow.conversation.QueryInputDTO;
import com.example.util.ProtobufUtil;
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
import com.google.cloud.dialogflow.cx.v3.EventInput;
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
import com.google.cloud.dialogflow.cx.v3.QueryInput;
import com.google.cloud.dialogflow.cx.v3.QueryParameters;
import com.google.cloud.dialogflow.cx.v3.TextInput;

View File

@@ -39,6 +39,7 @@ public class FirestoreConversationMapper {
private static final String FIELD_MESSAGES = "mensajes";
private static final String FIELD_PANTALLA_CONTEXTO = "pantallaContexto";
// Constants for fields within the 'mensajes' sub-documents
private static final String FIELD_MESSAGE_ENTITY = "entidad";
private static final String FIELD_MESSAGE_TYPE = "tipo";
@@ -75,6 +76,8 @@ public class FirestoreConversationMapper {
sessionMap.put(FIELD_PANTALLA_CONTEXTO, pantallaContexto);
}
sessionMap.put(FIELD_CREATED_AT, Timestamp.of(java.util.Date.from(Instant.now())));
sessionMap.put(FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())));
@@ -85,6 +88,8 @@ public class FirestoreConversationMapper {
return sessionMap;
}
private Map<String, Object> toFirestoreEntryMap(ConversationEntryDTO entry) {
Map<String, Object> entryMap = new HashMap<>();
entryMap.put(FIELD_MESSAGE_ENTITY, entry.entity().name());
@@ -125,6 +130,7 @@ public class FirestoreConversationMapper {
.map(this::mapFirestoreEntryMapToConversationEntryDTO)
.collect(Collectors.toList());
}
return new ConversationSessionDTO(sessionId, userId, telefono, createdAt, lastModified, entries, pantallaContexto);
}

View File

@@ -1,149 +0,0 @@
/*
* 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.mapper.quickreplies;
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
import com.example.dto.dialogflow.conversation.ConversationEntryEntity;
import com.example.dto.dialogflow.conversation.ConversationEntryType;
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
import com.google.cloud.Timestamp;
import com.google.cloud.firestore.FieldValue;
import com.google.cloud.firestore.DocumentSnapshot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class FirestoreQuickReplyMapper {
private static final Logger logger = LoggerFactory.getLogger(FirestoreQuickReplyMapper.class);
private static final String FIELD_SESSION_ID = "session_id";
private static final String FIELD_USER_ID = "usuario_id";
private static final String FIELD_PHONE_NUMBER = "telefono";
private static final String FIELD_CREATED_AT = "fechaCreacion";
private static final String FIELD_LAST_UPDATED = "ultimaActualizacion";
private static final String FIELD_MESSAGES = "mensajes";
private static final String FIELD_MESSAGE_ENTITY = "entidad";
private static final String FIELD_MESSAGE_TYPE = "tipo";
private static final String FIELD_MESSAGE_TEXT = "mensaje";
private static final String FIELD_MESSAGE_TIMESTAMP = "tiempo";
private static final String FIELD_MESSAGE_PARAMETERS = "parametros";
private static final String FIELD_MESSAGE_CHANNEL = "canal";
public Map<String, Object> createUpdateMapForSingleEntry(ConversationEntryDTO newEntry) {
Map<String, Object> updates = new HashMap<>();
Map<String, Object> entryMap = toFirestoreEntryMap(newEntry);
updates.put(FIELD_MESSAGES, FieldValue.arrayUnion(entryMap));
updates.put(FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())));
return updates;
}
public Map<String, Object> createNewSessionMapForSingleEntry(String sessionId, String userId, String telefono, ConversationEntryDTO initialEntry) {
Map<String, Object> sessionMap = new HashMap<>();
sessionMap.put(FIELD_SESSION_ID, sessionId);
sessionMap.put(FIELD_USER_ID, userId);
if (telefono != null && !telefono.trim().isEmpty()) {
sessionMap.put(FIELD_PHONE_NUMBER, telefono);
} else {
sessionMap.put(FIELD_PHONE_NUMBER, null);
}
sessionMap.put(FIELD_CREATED_AT, Timestamp.of(java.util.Date.from(Instant.now())));
sessionMap.put(FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())));
List<Map<String, Object>> entriesList = new ArrayList<>();
entriesList.add(toFirestoreEntryMap(initialEntry));
sessionMap.put(FIELD_MESSAGES, entriesList);
return sessionMap;
}
private Map<String, Object> toFirestoreEntryMap(ConversationEntryDTO entry) {
Map<String, Object> entryMap = new HashMap<>();
entryMap.put(FIELD_MESSAGE_ENTITY, entry.entity().name());
entryMap.put(FIELD_MESSAGE_TYPE, entry.type().name());
entryMap.put(FIELD_MESSAGE_TEXT, entry.text());
entryMap.put(FIELD_MESSAGE_TIMESTAMP, Timestamp.of(java.util.Date.from(entry.timestamp())));
if (entry.parameters() != null && !entry.parameters().isEmpty()) {
entryMap.put(FIELD_MESSAGE_PARAMETERS, entry.parameters());
}
if (entry.canal() != null) {
entryMap.put(FIELD_MESSAGE_CHANNEL, entry.canal());
}
return entryMap;
}
public ConversationSessionDTO mapFirestoreDocumentToConversationSessionDTO(DocumentSnapshot documentSnapshot) {
if (!documentSnapshot.exists()) {
return null;
}
String sessionId = documentSnapshot.getString(FIELD_SESSION_ID);
String userId = documentSnapshot.getString(FIELD_USER_ID);
String telefono = documentSnapshot.getString(FIELD_PHONE_NUMBER);
Timestamp createdAtFirestore = documentSnapshot.getTimestamp(FIELD_CREATED_AT);
Instant createdAt = (createdAtFirestore != null) ? createdAtFirestore.toDate().toInstant() : null;
Timestamp lastModifiedFirestore = documentSnapshot.getTimestamp(FIELD_LAST_UPDATED);
Instant lastModified = (lastModifiedFirestore != null) ? lastModifiedFirestore.toDate().toInstant() : null;
List<Map<String, Object>> rawEntries = (List<Map<String, Object>>) documentSnapshot.get(FIELD_MESSAGES);
List<ConversationEntryDTO> entries = new ArrayList<>();
if (rawEntries != null) {
entries = rawEntries.stream()
.map(this::mapFirestoreEntryMapToConversationEntryDTO)
.collect(Collectors.toList());
}
return new ConversationSessionDTO(sessionId, userId, telefono, createdAt, lastModified, entries, null);
}
private ConversationEntryDTO mapFirestoreEntryMapToConversationEntryDTO(Map<String, Object> entryMap) {
ConversationEntryEntity entity = null;
Object entityObj = entryMap.get(FIELD_MESSAGE_ENTITY);
if (entityObj instanceof String) {
try {
entity = ConversationEntryEntity.valueOf((String) entityObj);
} catch (IllegalArgumentException e) {
logger.warn("Unknown ConversationEntryEntity encountered: {}. Setting entity to null.", entityObj);
}
}
ConversationEntryType type = null;
Object typeObj = entryMap.get(FIELD_MESSAGE_TYPE);
if (typeObj instanceof String) {
try {
type = ConversationEntryType.valueOf((String) typeObj);
} catch (IllegalArgumentException e) {
logger.warn("Unknown ConversationEntryType encountered: {}. Setting type to null.", typeObj);
}
}
String text = (String) entryMap.get(FIELD_MESSAGE_TEXT);
Timestamp timestampFirestore = (Timestamp) entryMap.get(FIELD_MESSAGE_TIMESTAMP);
Instant timestamp = (timestampFirestore != null) ? timestampFirestore.toDate().toInstant() : null;
Map<String, Object> parameters = (Map<String, Object>) entryMap.get(FIELD_MESSAGE_PARAMETERS);
String canal = (String) entryMap.get(FIELD_MESSAGE_CHANNEL);
return new ConversationEntryDTO(entity, type, timestamp, text, parameters, canal);
}
}

View File

@@ -2,9 +2,7 @@
* 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.conversation;
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
import com.example.dto.dialogflow.conversation.ConversationContext;
@@ -24,22 +22,20 @@ import com.example.service.quickreplies.QuickRepliesManagerService;
import com.example.util.SessionIdGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@Service
public class ConversationManagerService {
private static final Logger logger = LoggerFactory.getLogger(ConversationManagerService.class);
private static final long SESSION_RESET_THRESHOLD_HOURS = 24;
private static final String CURRENT_PAGE_PARAM = "currentPage";
private final ExternalConvRequestMapper externalRequestToDialogflowMapper;
private final DialogflowClientService dialogflowServiceClient;
private final FirestoreConversationService firestoreConversationService;
@@ -49,6 +45,9 @@ public class ConversationManagerService {
private final MemoryStoreNotificationService memoryStoreNotificationService;
private final NotificationContextMapper notificationContextMapper;
private final ConversationContextMapper conversationContextMapper;
private final DataLossPrevention dataLossPrevention;
private final String dlpTemplateCompleteFlow;
private final String dlpTemplatePersistFlow;
public ConversationManagerService(
DialogflowClientService dialogflowServiceClient,
@@ -59,7 +58,10 @@ public class ConversationManagerService {
MessageEntryFilter messageEntryFilter,
MemoryStoreNotificationService memoryStoreNotificationService,
NotificationContextMapper notificationContextMapper,
ConversationContextMapper conversationContextMapper) {
ConversationContextMapper conversationContextMapper,
DataLossPrevention dataLossPrevention,
@Value("${google.cloud.dlp.dlpTemplateCompleteFlow}") String dlpTemplateCompleteFlow,
@Value("${google.cloud.dlp.dlpTemplatePersistFlow}") String dlpTemplatePersistFlow) {
this.dialogflowServiceClient = dialogflowServiceClient;
this.firestoreConversationService = firestoreConversationService;
this.memoryStoreConversationService = memoryStoreConversationService;
@@ -69,28 +71,31 @@ public class ConversationManagerService {
this.memoryStoreNotificationService = memoryStoreNotificationService;
this.notificationContextMapper = notificationContextMapper;
this.conversationContextMapper = conversationContextMapper;
this.dataLossPrevention = dataLossPrevention;
this.dlpTemplateCompleteFlow = dlpTemplateCompleteFlow;
this.dlpTemplatePersistFlow = dlpTemplatePersistFlow;
}
public Mono<DetectIntentResponseDTO> manageConversation(ExternalConvRequestDTO externalrequest) {
return memoryStoreConversationService.getSessionByTelefono(externalrequest.user().telefono())
.flatMap(session -> {
if (session != null && !session.entries().isEmpty()) {
ConversationEntryDTO lastEntry = session.entries().get(session.entries().size() - 1);
if (lastEntry.entity() == ConversationEntryEntity.SISTEMA && lastEntry.type() == ConversationEntryType.INICIO) {
logger.info("Detected 'SISTEMA' and 'INICIO' values in last session entry. Delegating to QuickRepliesManagerService.");
ExternalConvRequestDTO updatedRequest = new ExternalConvRequestDTO(
externalrequest.message(),
return dataLossPrevention.getObfuscatedString(externalrequest.message(), dlpTemplateCompleteFlow)
.flatMap(obfuscatedMessage -> {
ExternalConvRequestDTO obfuscatedRequest = new ExternalConvRequestDTO(
obfuscatedMessage,
externalrequest.user(),
externalrequest.channel(),
externalrequest.tipo(),
session.pantallaContexto()
externalrequest.pantallaContexto()
);
return quickRepliesManagerService.manageConversation(updatedRequest);
return memoryStoreConversationService.getSessionByTelefono(externalrequest.user().telefono())
.flatMap(session -> {
if (session != null && session.pantallaContexto() != null && !session.pantallaContexto().isBlank()) {
logger.info("Detected 'pantallaContexto' in session. Delegating to QuickRepliesManagerService.");
return quickRepliesManagerService.manageConversation(obfuscatedRequest);
}
}
return continueManagingConversation(externalrequest);
return continueManagingConversation(obfuscatedRequest);
})
.switchIfEmpty(continueManagingConversation(externalrequest));
.switchIfEmpty(continueManagingConversation(obfuscatedRequest));
});
}
private Mono<DetectIntentResponseDTO> continueManagingConversation(ExternalConvRequestDTO externalrequest) {
@@ -114,11 +119,9 @@ public class ConversationManagerService {
return handleMessageClassification(context, request);
}
private Mono<DetectIntentResponseDTO> handleMessageClassification(ConversationContext context, DetectIntentRequestDTO request) {
final String userPhoneNumber = context.primaryPhoneNumber();
final String userMessageText = context.userMessageText();
return memoryStoreNotificationService.getNotificationIdForPhone(userPhoneNumber)
.flatMap(notificationId -> memoryStoreNotificationService.getCachedNotificationSession(notificationId))
.map(notificationSession -> notificationSession.notificaciones().stream()
@@ -142,7 +145,6 @@ public class ConversationManagerService {
})
.switchIfEmpty(continueConversationFlow(context, request));
}
private Mono<DetectIntentResponseDTO> continueConversationFlow(ConversationContext context, DetectIntentRequestDTO request) {
final String userId = context.userId();
final String userMessageText = context.userMessageText();
@@ -190,7 +192,11 @@ public class ConversationManagerService {
private Mono<DetectIntentResponseDTO> processDialogflowRequest(ConversationSessionDTO session, DetectIntentRequestDTO request, String userId, String userMessageText, String userPhoneNumber, boolean newSession) {
final String finalSessionId = session.sessionId();
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText);
return dataLossPrevention.getObfuscatedString(userMessageText, dlpTemplatePersistFlow)
.flatMap(obfuscatedUserMessageText -> {
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(obfuscatedUserMessageText);
return this.persistConversationTurn(userId, finalSessionId, userEntry, userPhoneNumber)
.doOnSuccess(v -> logger.debug("User entry successfully persisted for session {}. Proceeding to Dialogflow...", finalSessionId))
@@ -204,13 +210,12 @@ public class ConversationManagerService {
})
.doOnError(error -> logger.error("Overall error during conversation management for session {}: {}", finalSessionId, error.getMessage(), error))
));
});
}
private Mono<DetectIntentResponseDTO> startNotificationConversation(ConversationContext context, DetectIntentRequestDTO request, NotificationDTO notification) {
final String userId = context.userId();
final String userMessageText = context.userMessageText();
final String userPhoneNumber = context.primaryPhoneNumber();
return memoryStoreNotificationService.getSessionByTelefono(userPhoneNumber)
.switchIfEmpty(Mono.defer(() -> {
String newSessionId = SessionIdGenerator.generateStandardSessionId();
@@ -229,11 +234,9 @@ public class ConversationManagerService {
}));
});
}
private Mono<Void> persistConversationTurn(String userId, String sessionId, ConversationEntryDTO entry,String userPhoneNumber) {
logger.debug("Starting Write-Back persistence for session {}. Type: {}. Writing to Redis first.", sessionId,
entry.type().name());
return memoryStoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
.doOnSuccess(v -> logger.info(
"Entry saved to Redis for session {}. Type: {}. Kicking off async Firestore write-back.",
@@ -248,12 +251,10 @@ public class ConversationManagerService {
.doOnError(e -> logger.error("Error during primary Redis write for session {}. Type: {}: {}", sessionId,
entry.type().name(), e.getMessage(), e));
}
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
Map<String, Object> params = Optional.ofNullable(request.queryParams())
.map(queryParamsDTO -> queryParamsDTO.parameters())
.orElse(Collections.emptyMap());
String primaryPhoneNumber = null;
Object telefonoObj = params.get("telefono"); // Get from map
if (telefonoObj instanceof String) {
@@ -262,12 +263,10 @@ public class ConversationManagerService {
logger.warn("Parameter 'telefono' in queryParams is not a String (type: {}). Expected String.",
telefonoObj.getClass().getName());
}
if (primaryPhoneNumber == null || primaryPhoneNumber.trim().isEmpty()) {
throw new IllegalArgumentException(
"Phone number (telefono) is required in query parameters for conversation management.");
}
String resolvedUserId = null;
Object userIdObj = params.get("usuario_id");
if (userIdObj instanceof String) {
@@ -276,18 +275,15 @@ public class ConversationManagerService {
logger.warn("Parameter 'userId' in query_params is not a String (type: {}). Expected String.",
userIdObj.getClass().getName());
}
if (resolvedUserId == null || resolvedUserId.trim().isEmpty()) {
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", "");
logger.warn("User ID not provided in query parameters. Using derived ID from phone number: {}",
resolvedUserId);
}
if (request.queryInput() == null || request.queryInput().text() == null ||
request.queryInput().text().text() == null || request.queryInput().text().text().trim().isEmpty()) {
throw new IllegalArgumentException("Dialogflow query input text is required.");
}
String userMessageText = request.queryInput().text().text();
return new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
}

View File

@@ -0,0 +1,7 @@
package com.example.service.conversation;
import reactor.core.publisher.Mono;
public interface DataLossPrevention {
Mono<String> getObfuscatedString(String textToInspect, String templateId);
}

View File

@@ -0,0 +1,98 @@
package com.example.service.conversation;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.google.cloud.dlp.v2.DlpServiceClient;
import com.google.privacy.dlp.v2.ByteContentItem;
import com.google.privacy.dlp.v2.ContentItem;
import com.google.privacy.dlp.v2.InspectConfig;
import com.google.privacy.dlp.v2.InspectContentRequest;
import com.google.privacy.dlp.v2.InspectContentResponse;
import com.google.privacy.dlp.v2.Likelihood;
import com.google.privacy.dlp.v2.LocationName;
import com.google.protobuf.ByteString;
import com.example.util.TextObfuscator;
import reactor.core.publisher.Mono;
@Service
public class DataLossPreventionImpl implements DataLossPrevention {
private static final Logger logger = LoggerFactory.getLogger(DataLossPreventionImpl.class);
private final String projectId;
private final String location;
private final DlpServiceClient dlpServiceClient;
public DataLossPreventionImpl(
DlpServiceClient dlpServiceClient,
@Value("${google.cloud.project}") String projectId,
@Value("${google.cloud.location}") String location) {
this.dlpServiceClient = dlpServiceClient;
this.projectId = projectId;
this.location = location;
}
@Override
public Mono<String> getObfuscatedString(String text, String templateId) {
ByteContentItem byteContentItem = ByteContentItem.newBuilder()
.setType(ByteContentItem.BytesType.TEXT_UTF8)
.setData(ByteString.copyFromUtf8(text))
.build();
ContentItem contentItem = ContentItem.newBuilder().setByteItem(byteContentItem).build();
Likelihood minLikelihood = Likelihood.VERY_UNLIKELY;
InspectConfig.FindingLimits findingLimits = InspectConfig.FindingLimits.newBuilder().setMaxFindingsPerItem(0)
.build();
InspectConfig inspectConfig = InspectConfig.newBuilder()
.setMinLikelihood(minLikelihood)
.setLimits(findingLimits)
.setIncludeQuote(true)
.build();
String inspectTemplateName = String.format("projects/%s/locations/%s/inspectTemplates/%s", projectId, location,
templateId);
InspectContentRequest request = InspectContentRequest.newBuilder()
.setParent(LocationName.of(projectId, location).toString())
.setInspectTemplateName(inspectTemplateName)
.setInspectConfig(inspectConfig)
.setItem(contentItem)
.build();
ApiFuture<InspectContentResponse> futureResponse = dlpServiceClient.inspectContentCallable()
.futureCall(request);
return Mono.<InspectContentResponse>create(
sink -> ApiFutures.addCallback(
futureResponse,
new ApiFutureCallback<>() {
@Override
public void onFailure(Throwable t) {
sink.error(t);
}
@Override
public void onSuccess(InspectContentResponse result) {
sink.success(result);
}
},
Runnable::run))
.map(response -> {
logger.info("DLP {} Findings: {}", templateId, response.getResult().getFindingsCount());
return response.getResult().getFindingsCount() > 0
? TextObfuscator.obfuscate(response, text)
: text;
}).onErrorResume(e -> {
e.printStackTrace();
return Mono.just(text);
});
}
}

View File

@@ -115,6 +115,8 @@ public class FirestoreConversationService {
});
}
private String getConversationCollectionPath() {
return String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
}

View File

@@ -93,4 +93,10 @@ public class MemoryStoreConversationService {
})
.doOnError(e -> logger.error("Error retrieving session by phone number {}: {}", telefono, e.getMessage(), e));
}
public Mono<Void> updateSession(ConversationSessionDTO session) {
String sessionKey = SESSION_KEY_PREFIX + session.sessionId();
logger.info("Attempting to update session {} in Redis.", session.sessionId());
return redisTemplate.opsForValue().set(sessionKey, session, SESSION_TTL).then();
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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.quickreplies;
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
import com.example.exception.FirestorePersistenceException;
import com.example.mapper.quickreplies.FirestoreQuickReplyMapper;
import com.example.repository.FirestoreBaseRepository;
import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.WriteBatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@Service
public class FirestoreQRService {
private static final Logger logger = LoggerFactory.getLogger(FirestoreQRService.class);
private static final String CONVERSATION_COLLECTION_PATH_FORMAT = "artifacts/%s/quick-replies-conversations";
private final FirestoreBaseRepository firestoreBaseRepository;
private final FirestoreQuickReplyMapper firestoreQuickReplyMapper;
public FirestoreQRService(FirestoreBaseRepository firestoreBaseRepository, FirestoreQuickReplyMapper firestoreQuickReplyMapper) {
this.firestoreBaseRepository = firestoreBaseRepository;
this.firestoreQuickReplyMapper = firestoreQuickReplyMapper;
}
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
logger.info("Attempting to save quick reply entry to Firestore for session {}. Entity: {}", sessionId, newEntry.entity().name());
return Mono.fromRunnable(() -> {
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
WriteBatch batch = firestoreBaseRepository.createBatch();
try {
if (firestoreBaseRepository.documentExists(sessionDocRef)) {
Map<String, Object> updates = firestoreQuickReplyMapper.createUpdateMapForSingleEntry(newEntry);
batch.update(sessionDocRef, updates);
logger.info("Appending entry to existing quick reply session for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
} else {
Map<String, Object> newSessionMap = firestoreQuickReplyMapper.createNewSessionMapForSingleEntry(sessionId, userId, userPhoneNumber, newEntry);
batch.set(sessionDocRef, newSessionMap);
logger.info("Creating new quick reply session with first entry for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
}
firestoreBaseRepository.commitBatch(batch);
logger.info("Successfully committed batch for session {} to Firestore.", sessionId);
} catch (ExecutionException e) {
logger.error("Error saving quick reply entry to Firestore for session {}: {}", sessionId, e.getMessage(), e);
throw new FirestorePersistenceException("Failed to save quick reply entry to Firestore for session " + sessionId, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("Thread interrupted while saving quick reply entry to Firestore for session {}: {}", sessionId, e.getMessage(), e);
throw new FirestorePersistenceException("Saving quick reply entry was interrupted for session " + sessionId, e);
}
}).subscribeOn(Schedulers.boundedElastic()).then();
}
public Mono<ConversationSessionDTO> getSessionByTelefono(String userPhoneNumber) {
logger.info("Attempting to retrieve quick reply session for phone number {}.", userPhoneNumber);
return firestoreBaseRepository.getDocumentsByField(getConversationCollectionPath(), "userPhoneNumber", userPhoneNumber)
.map(documentSnapshot -> {
if (documentSnapshot != null && documentSnapshot.exists()) {
ConversationSessionDTO sessionDTO = firestoreQuickReplyMapper.mapFirestoreDocumentToConversationSessionDTO(documentSnapshot);
logger.info("Successfully retrieved and mapped quick reply session for session {}.", sessionDTO.sessionId());
return sessionDTO;
}
logger.info("Quick reply session not found for phone number {}.", userPhoneNumber);
return null;
});
}
private String getConversationCollectionPath() {
return String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
}
private DocumentReference getSessionDocumentReference(String sessionId) {
String collectionPath = getConversationCollectionPath();
return firestoreBaseRepository.getDocumentReference(collectionPath, sessionId);
}
}

View File

@@ -33,11 +33,13 @@ public class MemoryStoreQRService {
this.stringRedisTemplate = stringRedisTemplate;
}
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry,
String userPhoneNumber) {
String sessionKey = SESSION_KEY_PREFIX + sessionId;
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + userPhoneNumber;
logger.info("Attempting to save entry to Redis for quick reply session {}. Entity: {}", sessionId, newEntry.entity().name());
logger.info("Attempting to save entry to Redis for quick reply session {}. Entity: {}", sessionId,
newEntry.entity().name());
return redisTemplate.opsForValue().get(sessionKey)
.defaultIfEmpty(ConversationSessionDTO.create(sessionId, userId, userPhoneNumber))
@@ -45,16 +47,20 @@ public class MemoryStoreQRService {
ConversationSessionDTO sessionWithUpdatedTelefono = session.withTelefono(userPhoneNumber);
ConversationSessionDTO updatedSession = sessionWithUpdatedTelefono.withAddedEntry(newEntry);
logger.info("Attempting to set updated quick reply session {} with new entry entity {} in Redis.", sessionId, newEntry.entity().name());
logger.info("Attempting to set updated quick reply session {} with new entry entity {} in Redis.",
sessionId, newEntry.entity().name());
return redisTemplate.opsForValue().set(sessionKey, updatedSession, SESSION_TTL)
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, SESSION_TTL))
.then();
})
.doOnSuccess(success -> {
logger.info("Successfully saved updated quick reply session and phone mapping to Redis for session {}. Entity Type: {}", sessionId, newEntry.entity().name());
logger.info(
"Successfully saved updated quick reply session and phone mapping to Redis for session {}. Entity Type: {}",
sessionId, newEntry.entity().name());
})
.doOnError(e -> logger.error("Error appending entry to Redis for quick reply session {}: {}", sessionId, e.getMessage(), e));
.doOnError(e -> logger.error("Error appending entry to Redis for quick reply session {}: {}", sessionId,
e.getMessage(), e));
}
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
@@ -65,16 +71,19 @@ public class MemoryStoreQRService {
logger.debug("Attempting to retrieve quick reply session ID for phone number {} from Redis.", telefono);
return stringRedisTemplate.opsForValue().get(phoneToSessionKey)
.flatMap(sessionId -> {
logger.debug("Found quick reply session ID {} for phone number {}. Retrieving session data.", sessionId, telefono);
logger.debug("Found quick reply session ID {} for phone number {}. Retrieving session data.",
sessionId, telefono);
return redisTemplate.opsForValue().get(SESSION_KEY_PREFIX + sessionId);
})
.doOnSuccess(session -> {
if (session != null) {
logger.info("Successfully retrieved quick reply session {} by phone number {}.", session.sessionId(), telefono);
logger.info("Successfully retrieved quick reply session {} by phone number {}.",
session.sessionId(), telefono);
} else {
logger.info("No quick reply session found in Redis for phone number {}.", telefono);
}
})
.doOnError(e -> logger.error("Error retrieving quick reply session by phone number {}: {}", telefono, e.getMessage(), e));
.doOnError(e -> logger.error("Error retrieving quick reply session by phone number {}: {}", telefono,
e.getMessage(), e));
}
}

View File

@@ -11,42 +11,50 @@ 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;
import com.example.service.conversation.MemoryStoreConversationService;
import com.example.util.SessionIdGenerator;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class QuickRepliesManagerService {
private static final Logger logger = LoggerFactory.getLogger(QuickRepliesManagerService.class);
private final MemoryStoreConversationService memoryStoreConversationService;
private final FirestoreConversationService firestoreConversationService;
private final QuickReplyContentService quickReplyContentService;
private final DataLossPrevention dataLossPrevention;
private final String dlpTemplatePersistFlow;
public QuickRepliesManagerService(
MemoryStoreConversationService memoryStoreConversationService,
FirestoreConversationService firestoreConversationService,
QuickReplyContentService quickReplyContentService) {
QuickReplyContentService quickReplyContentService,
DataLossPrevention dataLossPrevention,
@Value("${google.cloud.dlp.dlpTemplatePersistFlow}") String dlpTemplatePersistFlow) {
this.memoryStoreConversationService = memoryStoreConversationService;
this.firestoreConversationService = firestoreConversationService;
this.quickReplyContentService = quickReplyContentService;
this.dataLossPrevention = dataLossPrevention;
this.dlpTemplatePersistFlow = dlpTemplatePersistFlow;
}
public Mono<DetectIntentResponseDTO> startQuickReplySession(QuickReplyScreenRequestDTO externalRequest) {
String userPhoneNumber = externalRequest.user().telefono();
if (userPhoneNumber == null || userPhoneNumber.isBlank()) {
logger.warn("No phone number provided in request. Cannot manage conversation session without it.");
return Mono.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
return Mono
.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
}
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
.flatMap(session -> Mono.just(session.sessionId()))
.switchIfEmpty(Mono.fromCallable(SessionIdGenerator::generateStandardSessionId))
@@ -58,10 +66,9 @@ public class QuickRepliesManagerService {
Instant.now(),
"Pantalla :" + externalRequest.pantallaContexto() + " Agregada a la conversacion :",
null,
null
);
return persistConversationTurn(userId, sessionId, systemEntry, userPhoneNumber, externalRequest.pantallaContexto())
null);
return persistConversationTurn(userId, sessionId, systemEntry, userPhoneNumber,
externalRequest.pantallaContexto())
.then(quickReplyContentService.getQuickReplies(externalRequest.pantallaContexto()))
.map(quickReplyDTO -> new DetectIntentResponseDTO(sessionId, null, quickReplyDTO));
});
@@ -71,34 +78,93 @@ public class QuickRepliesManagerService {
String userPhoneNumber = externalRequest.user().telefono();
if (userPhoneNumber == null || userPhoneNumber.isBlank()) {
logger.warn("No phone number provided in request. Cannot manage conversation session without it.");
return Mono.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
return Mono
.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
}
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
.switchIfEmpty(Mono.error(new IllegalStateException("No quick reply session found for phone number: " + userPhoneNumber)))
.switchIfEmpty(Mono.error(
new IllegalStateException("No quick reply session found for phone number: " + userPhoneNumber)))
.flatMap(session -> {
String userId = session.userId();
String sessionId = session.sessionId();
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(externalRequest.message());
return dataLossPrevention.getObfuscatedString(externalRequest.message(), dlpTemplatePersistFlow)
.flatMap(obfuscatedMessage -> {
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(obfuscatedMessage);
return persistConversationTurn(userId, sessionId, userEntry, userPhoneNumber, session.pantallaContexto())
long userMessagesCount = session.entries().stream()
.filter(e -> e.entity() == ConversationEntryEntity.USUARIO)
.count();
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.
return persistConversationTurn(userId, sessionId, userEntry, userPhoneNumber,
session.pantallaContexto())
.then(quickReplyContentService.getQuickReplies(session.pantallaContexto()))
.flatMap(quickReplyDTO -> {
ConversationEntryDTO agentEntry = ConversationEntryDTO.forAgentWithMessage(quickReplyDTO.toString());
return persistConversationTurn(userId, sessionId, agentEntry, userPhoneNumber, session.pantallaContexto())
ConversationEntryDTO agentEntry = ConversationEntryDTO
.forAgentWithMessage(quickReplyDTO.toString());
return persistConversationTurn(userId, sessionId, agentEntry, userPhoneNumber,
session.pantallaContexto())
.thenReturn(new DetectIntentResponseDTO(sessionId, null, quickReplyDTO));
});
} else if (userMessagesCount == 1) { // Is the second user message in the QR flow
// This is the third message of the flow. Filter and end.
return persistConversationTurn(userId, sessionId, userEntry, userPhoneNumber,
session.pantallaContexto())
.then(quickReplyContentService.getQuickReplies(session.pantallaContexto()))
.flatMap(quickReplyDTO -> {
List<QuestionDTO> matchedPreguntas = quickReplyDTO.preguntas().stream()
.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());
}
// End the quick reply flow by clearing the pantallaContexto
return memoryStoreConversationService
.updateSession(session.withPantallaContexto(null))
.then(persistConversationTurn(userId, sessionId,
ConversationEntryDTO.forAgentWithMessage(
responseQuickReplyDTO.toString()),
userPhoneNumber, null))
.thenReturn(new DetectIntentResponseDTO(sessionId, null,
responseQuickReplyDTO));
});
} 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()))));
}
});
});
}
private Mono<Void> persistConversationTurn(String userId, String sessionId, ConversationEntryDTO entry, String userPhoneNumber, String pantallaContexto) {
logger.debug("Starting Write-Back persistence for quick reply session {}. Type: {}. Writing to Redis first.", sessionId, entry.type().name());
private Mono<Void> persistConversationTurn(String userId, String sessionId, ConversationEntryDTO entry,
String userPhoneNumber, String pantallaContexto) {
logger.debug("Starting Write-Back persistence for quick reply session {}. Type: {}. Writing to Redis first.",
sessionId, entry.type().name());
return memoryStoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber, pantallaContexto)
.doOnSuccess(v -> logger.info("Entry saved to Redis for quick reply session {}. Type: {}. Kicking off async Firestore write-back.", sessionId, entry.type().name()))
.then(firestoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber, pantallaContexto)
.doOnSuccess(fsVoid -> logger.debug("Asynchronously (Write-Back): Entry successfully saved to Firestore for quick reply session {}. Type: {}.", sessionId, entry.type().name()))
.doOnError(fsError -> logger.error("Asynchronously (Write-Back): Failed to save entry to Firestore for quick reply session {}. Type: {}: {}", sessionId, entry.type().name(), fsError.getMessage(), fsError)))
.doOnError(e -> logger.error("Error during primary Redis write for quick reply session {}. Type: {}: {}", sessionId, entry.type().name(), e.getMessage(), e));
.doOnSuccess(v -> logger.info(
"Entry saved to Redis for quick reply session {}. Type: {}. Kicking off async Firestore write-back.",
sessionId, entry.type().name()))
.then(firestoreConversationService
.saveEntry(userId, sessionId, entry, userPhoneNumber, pantallaContexto)
.doOnSuccess(fsVoid -> logger.debug(
"Asynchronously (Write-Back): Entry successfully saved to Firestore for quick reply session {}. Type: {}.",
sessionId, entry.type().name()))
.doOnError(fsError -> logger.error(
"Asynchronously (Write-Back): Failed to save entry to Firestore for quick reply session {}. Type: {}: {}",
sessionId, entry.type().name(), fsError.getMessage(), fsError)))
.doOnError(
e -> logger.error("Error during primary Redis write for quick reply session {}. Type: {}: {}",
sessionId, entry.type().name(), e.getMessage(), e));
}
}

View File

@@ -9,33 +9,27 @@ import com.example.dto.quickreplies.QuestionDTO;
import com.example.dto.quickreplies.QuickReplyDTO;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
@Service
public class QuickReplyContentService {
private static final Logger logger = LoggerFactory.getLogger(QuickReplyContentService.class);
private final Firestore firestore;
public QuickReplyContentService(Firestore firestore) {
this.firestore = firestore;
}
public Mono<QuickReplyDTO> getQuickReplies(String collectionId) {
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.fromCallable(() -> {
try {
return firestore.collection("artifacts")
@@ -50,8 +44,12 @@ public class QuickReplyContentService {
})
.filter(DocumentSnapshot::exists)
.map(document -> {
QuestionDTO pregunta = new QuestionDTO(document.getString("titulo"), document.getString("descripcion"));
return new QuickReplyDTO("preguntas sobre " + collectionId, List.of(pregunta));
String header = document.getString("header");
List<Map<String, Object>> preguntasData = (List<Map<String, Object>>) document.get("preguntas");
List<QuestionDTO> 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);
})
.doOnSuccess(quickReplyDTO -> {
if (quickReplyDTO != null) {

View File

@@ -0,0 +1,82 @@
package com.example.util;
import java.util.Comparator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.privacy.dlp.v2.Finding;
import com.google.privacy.dlp.v2.InspectContentResponse;
public class TextObfuscator {
private static final Logger logger = LoggerFactory.getLogger(TextObfuscator.class);
public static String obfuscate(InspectContentResponse response, String textToInspect) {
List<Finding> findings = response.getResult().getFindingsList().stream()
.filter(finding -> finding.getLikelihoodValue() > 3)
.sorted(Comparator.comparing(Finding::getLikelihoodValue).reversed())
.peek(finding -> logger.info("InfoType: {} | Likelihood: {}", finding.getInfoType().getName(),
finding.getLikelihoodValue()))
.toList();
for (Finding finding : findings) {
String quote = finding.getQuote();
switch (finding.getInfoType().getName()) {
case "CREDIT_CARD_NUMBER":
textToInspect = textToInspect.replace(quote, "**** **** **** " + getLast4(quote));
break;
case "CREDIT_CARD_EXPIRATION_DATE":
case "FECHA_VENCIMIENTO":
textToInspect = textToInspect.replace(quote, "[FECHA_VENCIMIENTO_TARJETA]");
break;
case "CVV_NUMBER":
case "CVV":
textToInspect = textToInspect.replace(quote, "[CVV]");
break;
case "EMAIL_ADDRESS":
textToInspect = textToInspect.replace(quote, "[CORREO]");
break;
case "PERSON_NAME":
textToInspect = textToInspect.replace(quote, "[NOMBRE]");
break;
case "PHONE_NUMBER":
textToInspect = textToInspect.replace(quote, "[TELEFONO]");
break;
case "DIRECCION":
case "STREET_ADDRESS":
textToInspect = textToInspect.replace(quote, "[DIRECCION]");
break;
case "CLABE_INTERBANCARIA":
textToInspect = textToInspect.replace(quote, "[CLABE]");
break;
case "CLAVE_RASTREO_SPEI":
textToInspect = textToInspect.replace(quote, "[CLAVE_RASTREO]");
break;
case "NIP":
textToInspect = textToInspect.replace(quote, "[NIP]");
break;
case "SALDO":
textToInspect = textToInspect.replace(quote, "[SALDO]");
break;
case "CUENTA":
textToInspect = textToInspect.replace(quote, "**************" + getLast4(quote));
break;
case "NUM_ACLARACION":
textToInspect = textToInspect.replace(quote, "[NUM_ACLARACION]");
break;
}
}
return textToInspect;
}
private static String getLast4(String quote) {
char[] last4 = new char[4];
String cleanQuote = quote.trim();
cleanQuote = cleanQuote.replace(" ", "");
cleanQuote.getChars(cleanQuote.length() - 4, cleanQuote.length(), last4, 0);
return new String(last4);
}
}

View File

@@ -60,3 +60,8 @@ messagefilter.temperature=0.1f
messagefilter.maxOutputTokens=800
messagefilter.topP= 0.1f
messagefilter.prompt=prompts/message_filter_prompt.txt
# =========================================================
# (DLP) Configuration
# =========================================================
google.cloud.dlp.dlpTemplateCompleteFlow=IMC_INSPECT_USC
google.cloud.dlp.dlpTemplatePersistFlow=IMC_INSPECT_NAME

View File

@@ -1,4 +1,15 @@
{
"titulo": "Payment History",
"descripcion": "View your recent payments"
"header": "preguntas sobre pagos",
"preguntas": [
{
"titulo": "Donde veo mi historial de pagos?",
"descripcion": "View your recent payments",
"respuesta": "puedes visualizar esto en la opcion X de tu app"
},
{
"titulo": "Pregunta servicio A",
"descripcion": "descripcion servicio A",
"respuesta": "puedes ver info de servicio A en tu app"
}
]
}

View File

@@ -0,0 +1,133 @@
package com.example.mapper.conversation;
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
import com.example.dto.dialogflow.conversation.QueryInputDTO;
import com.example.dto.dialogflow.conversation.QueryParamsDTO;
import com.example.dto.dialogflow.conversation.TextInputDTO;
import com.example.dto.dialogflow.notification.EventInputDTO;
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
import com.google.cloud.dialogflow.cx.v3.QueryInput;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class DialogflowRequestMapperTest {
@InjectMocks
private DialogflowRequestMapper dialogflowRequestMapper;
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(dialogflowRequestMapper, "defaultLanguageCode", "es");
}
@Test
void mapToDetectIntentRequestBuilder_withTextInput_shouldMapCorrectly() {
// Given
TextInputDTO textInputDTO = new TextInputDTO("Hola");
QueryInputDTO queryInputDTO = new QueryInputDTO(textInputDTO, null, "es");
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
// When
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
DetectIntentRequest request = builder.build();
// Then
assertNotNull(request);
assertTrue(request.hasQueryInput());
QueryInput queryInput = request.getQueryInput();
assertEquals("es", queryInput.getLanguageCode());
assertTrue(queryInput.hasText());
assertEquals("Hola", queryInput.getText().getText());
assertFalse(queryInput.hasEvent());
}
@Test
void mapToDetectIntentRequestBuilder_withEventInput_shouldMapCorrectly() {
// Given
EventInputDTO eventInputDTO = new EventInputDTO("welcome_event");
QueryInputDTO queryInputDTO = new QueryInputDTO(null, eventInputDTO, "es");
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
// When
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
DetectIntentRequest request = builder.build();
// Then
assertNotNull(request);
assertTrue(request.hasQueryInput());
QueryInput queryInput = request.getQueryInput();
assertEquals("es", queryInput.getLanguageCode());
assertTrue(queryInput.hasEvent());
assertEquals("welcome_event", queryInput.getEvent().getEvent());
assertFalse(queryInput.hasText());
}
@Test
void mapToDetectIntentRequestBuilder_withNoInput_shouldThrowException() {
// Given
QueryInputDTO queryInputDTO = new QueryInputDTO(null, null, "es");
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
// When & Then
assertThrows(IllegalArgumentException.class, () -> {
dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
});
}
@Test
void mapToDetectIntentRequestBuilder_withParameters_shouldMapCorrectly() {
// Given
TextInputDTO textInputDTO = new TextInputDTO("Hola");
QueryInputDTO queryInputDTO = new QueryInputDTO(textInputDTO, null, "es");
Map<String, Object> parameters = Collections.singletonMap("param1", "value1");
QueryParamsDTO queryParamsDTO = new QueryParamsDTO(parameters);
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, queryParamsDTO);
// When
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
DetectIntentRequest request = builder.build();
// Then
assertNotNull(request);
assertTrue(request.hasQueryParams());
assertTrue(request.getQueryParams().hasParameters());
assertEquals("value1", request.getQueryParams().getParameters().getFieldsMap().get("param1").getStringValue());
}
@Test
void mapToDetectIntentRequestBuilder_withNullRequestDTO_shouldThrowException() {
// When & Then
assertThrows(NullPointerException.class, () -> {
dialogflowRequestMapper.mapToDetectIntentRequestBuilder(null);
});
}
@Test
void mapToDetectIntentRequestBuilder_withDefaultLanguageCode_shouldMapCorrectly() {
// Given
TextInputDTO textInputDTO = new TextInputDTO("Hola");
QueryInputDTO queryInputDTO = new QueryInputDTO(textInputDTO, null, null);
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
// When
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
DetectIntentRequest request = builder.build();
// Then
assertNotNull(request);
assertTrue(request.hasQueryInput());
assertEquals("es", request.getQueryInput().getLanguageCode());
}
}

View File

@@ -0,0 +1,129 @@
package com.example.mapper.conversation;
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
import com.example.dto.dialogflow.conversation.QueryResultDTO;
import com.google.cloud.dialogflow.cx.v3.DetectIntentResponse;
import com.google.cloud.dialogflow.cx.v3.QueryResult;
import com.google.cloud.dialogflow.cx.v3.ResponseMessage;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(MockitoExtension.class)
class DialogflowResponseMapperTest {
@InjectMocks
private DialogflowResponseMapper dialogflowResponseMapper;
@Test
void mapFromDialogflowResponse_shouldMapCorrectly() {
// Given
ResponseMessage.Text text1 = ResponseMessage.Text.newBuilder()
.addAllText(Collections.singletonList("Hello")).build();
ResponseMessage message1 = ResponseMessage.newBuilder().setText(text1).build();
ResponseMessage.Text text2 = ResponseMessage.Text.newBuilder()
.addAllText(Collections.singletonList("World")).build();
ResponseMessage message2 = ResponseMessage.newBuilder().setText(text2).build();
Struct params = Struct.newBuilder()
.putFields("param1", Value.newBuilder().setStringValue("value1").build())
.putFields("param2", Value.newBuilder().setNumberValue(123).build())
.build();
QueryResult queryResult = QueryResult.newBuilder()
.addAllResponseMessages(Arrays.asList(message1, message2))
.setParameters(params)
.build();
DetectIntentResponse detectIntentResponse = DetectIntentResponse.newBuilder()
.setResponseId("test-response-id")
.setQueryResult(queryResult)
.build();
// When
DetectIntentResponseDTO responseDTO = dialogflowResponseMapper
.mapFromDialogflowResponse(detectIntentResponse, "test-session-id");
// Then
assertNotNull(responseDTO);
assertEquals("test-response-id", responseDTO.responseId());
QueryResultDTO queryResultDTO = responseDTO.queryResult();
assertNotNull(queryResultDTO);
assertEquals("Hello World", queryResultDTO.responseText());
Map<String, Object> parameters = queryResultDTO.parameters();
assertNotNull(parameters);
assertEquals(2, parameters.size());
assertEquals("value1", parameters.get("param1"));
assertEquals(123.0, parameters.get("param2"));
}
@Test
void mapFromDialogflowResponse_withNoMessages_shouldReturnEmptyFulfillmentText() {
// Given
QueryResult queryResult = QueryResult.newBuilder()
.build();
DetectIntentResponse detectIntentResponse = DetectIntentResponse.newBuilder()
.setResponseId("test-response-id")
.setQueryResult(queryResult)
.build();
// When
DetectIntentResponseDTO responseDTO = dialogflowResponseMapper
.mapFromDialogflowResponse(detectIntentResponse, "test-session-id");
// Then
assertNotNull(responseDTO);
assertEquals("test-response-id", responseDTO.responseId());
QueryResultDTO queryResultDTO = responseDTO.queryResult();
assertNotNull(queryResultDTO);
assertEquals("", queryResultDTO.responseText());
}
@Test
void mapFromDialogflowResponse_withNoParameters_shouldReturnEmptyMap() {
// Given
ResponseMessage.Text text = ResponseMessage.Text.newBuilder()
.addAllText(Collections.singletonList("Hello")).build();
ResponseMessage message = ResponseMessage.newBuilder().setText(text).build();
QueryResult queryResult = QueryResult.newBuilder()
.addResponseMessages(message)
.build();
DetectIntentResponse detectIntentResponse = DetectIntentResponse.newBuilder()
.setResponseId("test-response-id")
.setQueryResult(queryResult)
.build();
// When
DetectIntentResponseDTO responseDTO = dialogflowResponseMapper
.mapFromDialogflowResponse(detectIntentResponse, "test-session-id");
// Then
assertNotNull(responseDTO);
assertEquals("test-response-id", responseDTO.responseId());
QueryResultDTO queryResultDTO = responseDTO.queryResult();
assertNotNull(queryResultDTO);
assertEquals("Hello", queryResultDTO.responseText());
Map<String, Object> parameters = queryResultDTO.parameters();
assertNotNull(parameters);
assertEquals(0, parameters.size());
}
}

View File

@@ -0,0 +1,118 @@
package com.example.service.unit_testing;
import com.example.dto.quickreplies.QuestionDTO;
import com.example.dto.quickreplies.QuickReplyDTO;
import com.example.service.quickreplies.QuickReplyContentService;
import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static org.mockito.Mockito.when;
public class QuickReplyContentServiceTest {
@Mock
private Firestore firestore;
@Mock
private CollectionReference collectionReference;
@Mock
private DocumentReference documentReference;
@Mock
private ApiFuture<DocumentSnapshot> apiFuture;
@Mock
private DocumentSnapshot documentSnapshot;
@InjectMocks
private QuickReplyContentService quickReplyContentService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void getQuickReplies_success() throws ExecutionException, InterruptedException {
// Given
String collectionId = "home";
String header = "home_header";
List<Map<String, Object>> preguntas = Collections.singletonList(
Map.of("titulo", "title", "descripcion", "description", "respuesta", "response")
);
List<QuestionDTO> questionDTOs = Collections.singletonList(
new QuestionDTO("title", "description", "response")
);
QuickReplyDTO expected = new QuickReplyDTO(header, questionDTOs);
when(firestore.collection("artifacts")).thenReturn(collectionReference);
when(collectionReference.document("default-app-id")).thenReturn(documentReference);
when(documentReference.collection("quick-replies")).thenReturn(collectionReference);
when(collectionReference.document(collectionId)).thenReturn(documentReference);
when(documentReference.get()).thenReturn(apiFuture);
when(apiFuture.get()).thenReturn(documentSnapshot);
when(documentSnapshot.exists()).thenReturn(true);
when(documentSnapshot.getString("header")).thenReturn(header);
when(documentSnapshot.get("preguntas")).thenReturn(preguntas);
// When
Mono<QuickReplyDTO> result = quickReplyContentService.getQuickReplies(collectionId);
// Then
StepVerifier.create(result)
.expectNext(expected)
.verifyComplete();
}
@Test
void getQuickReplies_emptyWhenNotFound() throws ExecutionException, InterruptedException {
// Given
String collectionId = "non-existent-collection";
when(firestore.collection("artifacts")).thenReturn(collectionReference);
when(collectionReference.document("default-app-id")).thenReturn(documentReference);
when(documentReference.collection("quick-replies")).thenReturn(collectionReference);
when(collectionReference.document(collectionId)).thenReturn(documentReference);
when(documentReference.get()).thenReturn(apiFuture);
when(apiFuture.get()).thenReturn(documentSnapshot);
when(documentSnapshot.exists()).thenReturn(false);
// When
Mono<QuickReplyDTO> result = quickReplyContentService.getQuickReplies(collectionId);
// Then
StepVerifier.create(result)
.verifyComplete();
}
@Test
void getQuickReplies_emptyWhenCollectionIdIsBlank() {
// Given
String collectionId = "";
// When
Mono<QuickReplyDTO> result = quickReplyContentService.getQuickReplies(collectionId);
// Then
StepVerifier.create(result)
.expectNext(new QuickReplyDTO("empty", Collections.emptyList()))
.verifyComplete();
}
}