diff --git a/src/main/java/com/example/dto/dialogflow/conversation/ConversationSessionDTO.java b/src/main/java/com/example/dto/dialogflow/conversation/ConversationSessionDTO.java index 146b439..ce1c9ed 100644 --- a/src/main/java/com/example/dto/dialogflow/conversation/ConversationSessionDTO.java +++ b/src/main/java/com/example/dto/dialogflow/conversation/ConversationSessionDTO.java @@ -9,9 +9,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) diff --git a/src/main/java/com/example/mapper/conversation/ConversationMessageMapper.java b/src/main/java/com/example/mapper/conversation/ConversationMessageMapper.java index 42f2a6a..38c37bb 100644 --- a/src/main/java/com/example/mapper/conversation/ConversationMessageMapper.java +++ b/src/main/java/com/example/mapper/conversation/ConversationMessageMapper.java @@ -18,11 +18,11 @@ public class ConversationMessageMapper { public Map toMap(ConversationMessageDTO message) { Map map = new HashMap<>(); - map.put("type", message.type().name()); - map.put("timestamp", message.timestamp()); - map.put("text", message.text()); + map.put("entidad", message.type().name()); + map.put("tiempo", message.timestamp()); + map.put("mensaje", message.text()); if (message.parameters() != null) { - map.put("parameters", message.parameters()); + map.put("parametros", message.parameters()); } if (message.canal() != null) { map.put("canal", message.canal()); @@ -32,10 +32,10 @@ public class ConversationMessageMapper { public ConversationMessageDTO fromMap(Map map) { return new ConversationMessageDTO( - MessageType.valueOf((String) map.get("type")), - (Instant) map.get("timestamp"), - (String) map.get("text"), - (Map) map.get("parameters"), + MessageType.valueOf((String) map.get("entidad")), + (Instant) map.get("tiempo"), + (String) map.get("mensaje"), + (Map) map.get("parametros"), (String) map.get("canal") ); } diff --git a/src/main/java/com/example/mapper/conversation/FirestoreConversationMapper.java b/src/main/java/com/example/mapper/conversation/FirestoreConversationMapper.java index 564e781..0398b48 100644 --- a/src/main/java/com/example/mapper/conversation/FirestoreConversationMapper.java +++ b/src/main/java/com/example/mapper/conversation/FirestoreConversationMapper.java @@ -22,8 +22,8 @@ public class FirestoreConversationMapper { return null; } - Timestamp createdAtTimestamp = document.getTimestamp("createdAt"); - Timestamp lastModifiedTimestamp = document.getTimestamp("lastModified"); + Timestamp createdAtTimestamp = document.getTimestamp("fechaCreacion"); + Timestamp lastModifiedTimestamp = document.getTimestamp("ultimaActualizacion"); Instant createdAt = (createdAtTimestamp != null) ? createdAtTimestamp.toDate().toInstant() : null; Instant lastModified = (lastModifiedTimestamp != null) ? lastModifiedTimestamp.toDate().toInstant() : null; @@ -34,7 +34,7 @@ public class FirestoreConversationMapper { document.getString("telefono"), createdAt, lastModified, - document.getString("lastMessage"), + document.getString("ultimoMensaje"), document.getString("pantallaContexto") ); } @@ -44,9 +44,9 @@ public class FirestoreConversationMapper { sessionMap.put("sessionId", session.sessionId()); sessionMap.put("userId", session.userId()); sessionMap.put("telefono", session.telefono()); - sessionMap.put("createdAt", session.createdAt()); - sessionMap.put("lastModified", session.lastModified()); - sessionMap.put("lastMessage", session.lastMessage()); + sessionMap.put("fechaCreacion", session.createdAt()); + sessionMap.put("ultimaActualizacion", session.lastModified()); + sessionMap.put("ultimoMensaje", session.lastMessage()); sessionMap.put("pantallaContexto", session.pantallaContexto()); return sessionMap; } diff --git a/src/main/java/com/example/mapper/messagefilter/ConversationContextMapper.java b/src/main/java/com/example/mapper/messagefilter/ConversationContextMapper.java index a3d28b5..9f6ce8f 100644 --- a/src/main/java/com/example/mapper/messagefilter/ConversationContextMapper.java +++ b/src/main/java/com/example/mapper/messagefilter/ConversationContextMapper.java @@ -1,36 +1,79 @@ -/* - * 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.messagefilter; import com.example.dto.dialogflow.conversation.ConversationMessageDTO; import com.example.dto.dialogflow.conversation.ConversationSessionDTO; +import com.example.dto.dialogflow.conversation.MessageType; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @Component public class ConversationContextMapper { - private static final int MAX_MESSAGES = 10; + @Value("${conversation.context.message.limit:60}") + private int messageLimit; + @Value("${conversation.context.days.limit:30}") + private int daysLimit; + public String toText(ConversationSessionDTO session, List messages) { - return toTextFromMessages(messages); - } - - public String toTextWithLimits(ConversationSessionDTO session, List messages) { - if (messages.size() > MAX_MESSAGES) { - messages = messages.subList(messages.size() - MAX_MESSAGES, messages.size()); + if (messages == null || messages.isEmpty()) { + return ""; } return toTextFromMessages(messages); } + public String toTextWithLimits(ConversationSessionDTO session, List messages) { + if (messages == null || messages.isEmpty()) { + return ""; + } + + Instant thirtyDaysAgo = Instant.now().minus(daysLimit, ChronoUnit.DAYS); + + List recentEntries = messages.stream() + .filter(entry -> entry.timestamp() != null && entry.timestamp().isAfter(thirtyDaysAgo)) + .sorted(Comparator.comparing(ConversationMessageDTO::timestamp).reversed()) + .limit(messageLimit) + .sorted(Comparator.comparing(ConversationMessageDTO::timestamp)) + .collect(Collectors.toList()); + return toTextFromMessages(recentEntries); + } + public String toTextFromMessages(List messages) { return messages.stream() - .map(message -> String.format("%s: %s", message.type(), message.text())) + .map(this::formatEntry) .collect(Collectors.joining("\n")); } + + private String formatEntry(ConversationMessageDTO entry) { + String prefix = "User: "; + + if (entry.type() != null) { + switch (entry.type()) { + case MessageType.AGENT: + prefix = "Agent: "; + break; + case MessageType.SYSTEM: + prefix = "System: "; + break; + case MessageType.USER: + default: + prefix = "User: "; + break; + } + } + + String text = prefix + entry.text(); + + if (entry.parameters() != null && !entry.parameters().isEmpty()) { + text += " " + entry.parameters().toString(); + } + + return text; + } } \ No newline at end of file diff --git a/src/main/java/com/example/repository/FirestoreBaseRepository.java b/src/main/java/com/example/repository/FirestoreBaseRepository.java index 1fd5bf0..772e1e6 100644 --- a/src/main/java/com/example/repository/FirestoreBaseRepository.java +++ b/src/main/java/com/example/repository/FirestoreBaseRepository.java @@ -220,4 +220,10 @@ public class FirestoreBaseRepository { throw new RuntimeException("Error deleting collection", e); } } + + public void deleteDocumentAndSubcollections(DocumentReference docRef, String subcollection) + throws ExecutionException, InterruptedException { + deleteCollection(docRef.collection(subcollection).getPath(), 50); + deleteDocument(docRef); + } } \ No newline at end of file diff --git a/src/main/java/com/example/service/base/DataPurgeService.java b/src/main/java/com/example/service/base/DataPurgeService.java index 4b9987c..2044852 100644 --- a/src/main/java/com/example/service/base/DataPurgeService.java +++ b/src/main/java/com/example/service/base/DataPurgeService.java @@ -1,106 +1,214 @@ package com.example.service.base; + + import com.example.repository.FirestoreBaseRepository; + import com.google.cloud.firestore.CollectionReference; + import com.google.cloud.firestore.Firestore; + import com.google.cloud.firestore.QueryDocumentSnapshot; + import java.util.List; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.beans.factory.annotation.Qualifier; + import org.springframework.data.redis.core.ReactiveRedisTemplate; + import org.springframework.stereotype.Service; + import reactor.core.publisher.Mono; + import reactor.core.scheduler.Schedulers; + + @Service + public class DataPurgeService { + + private static final Logger logger = LoggerFactory.getLogger(DataPurgeService.class); + + private final ReactiveRedisTemplate redisTemplate; + private final FirestoreBaseRepository firestoreBaseRepository; + + private final Firestore firestore; + + @Autowired + public DataPurgeService( + @Qualifier("reactiveRedisTemplate") ReactiveRedisTemplate redisTemplate, + FirestoreBaseRepository firestoreBaseRepository, Firestore firestore) { + this.redisTemplate = redisTemplate; + this.firestoreBaseRepository = firestoreBaseRepository; + this.firestore = firestore; + } + + public Mono purgeAllData() { + return purgeRedis() + .then(purgeFirestore()); + } + + private Mono purgeRedis() { + logger.info("Starting Redis data purge."); + return redisTemplate.getConnectionFactory().getReactiveConnection().serverCommands().flushAll() + .doOnSuccess(v -> logger.info("Successfully purged all data from Redis.")) + .doOnError(e -> logger.error("Error purging data from Redis.", e)) + .then(); + } + + private Mono purgeFirestore() { + logger.info("Starting Firestore data purge."); + return Mono.fromRunnable(() -> { + try { + String appId = firestoreBaseRepository.getAppId(); + String conversationsCollectionPath = String.format("artifacts/%s/conversations", appId); + String notificationsCollectionPath = String.format("artifacts/%s/notifications", appId); - // Delete 'messages' sub-collections in 'conversations' - logger.info("Deleting 'messages' sub-collections from '{}'", conversationsCollectionPath); + + + // Delete 'mensajes' sub-collections in 'conversations' + + logger.info("Deleting 'mensajes' sub-collections from '{}'", conversationsCollectionPath); + try { + List conversationDocuments = firestore.collection(conversationsCollectionPath).get().get().getDocuments(); + for (QueryDocumentSnapshot document : conversationDocuments) { - String messagesCollectionPath = document.getReference().getPath() + "/messages"; + + String messagesCollectionPath = document.getReference().getPath() + "/mensajes"; + logger.info("Deleting sub-collection: {}", messagesCollectionPath); + firestoreBaseRepository.deleteCollection(messagesCollectionPath, 50); + } + } catch (Exception e) { + if (e.getMessage().contains("NOT_FOUND")) { + logger.warn("Collection '{}' not found, skipping.", conversationsCollectionPath); + } else { + throw e; + } + } + + // Delete the 'conversations' collection + logger.info("Deleting collection: {}", conversationsCollectionPath); + try { + firestoreBaseRepository.deleteCollection(conversationsCollectionPath, 50); + } catch (Exception e) { + if (e.getMessage().contains("NOT_FOUND")) { + logger.warn("Collection '{}' not found, skipping.", conversationsCollectionPath); - } else { - throw e; + } + + else { + + throw e; + + } + } + + // Delete the 'notifications' collection + logger.info("Deleting collection: {}", notificationsCollectionPath); + try { + firestoreBaseRepository.deleteCollection(notificationsCollectionPath, 50); + } catch (Exception e) { + if (e.getMessage().contains("NOT_FOUND")) { + logger.warn("Collection '{}' not found, skipping.", notificationsCollectionPath); + } else { + throw e; + } + } + + logger.info("Successfully purged Firestore collections."); + } catch (Exception e) { + logger.error("Error purging Firestore collections.", e); + throw new RuntimeException("Failed to purge Firestore collections.", e); + } + }).subscribeOn(Schedulers.boundedElastic()).then(); + } + } + + diff --git a/src/main/java/com/example/service/base/SessionPurgeService.java b/src/main/java/com/example/service/base/SessionPurgeService.java index 4ab3d4d..0b7ab99 100644 --- a/src/main/java/com/example/service/base/SessionPurgeService.java +++ b/src/main/java/com/example/service/base/SessionPurgeService.java @@ -7,6 +7,7 @@ package com.example.service.base; import com.example.dto.dialogflow.conversation.ConversationSessionDTO; import com.example.service.conversation.FirestoreConversationService; +import com.example.service.conversation.MemoryStoreConversationService; import com.example.service.notification.FirestoreNotificationService; import com.example.service.notification.MemoryStoreNotificationService; import org.slf4j.Logger; @@ -21,10 +22,9 @@ public class SessionPurgeService { private static final Logger logger = LoggerFactory.getLogger(SessionPurgeService.class); private static final String SESSION_KEY_PREFIX = "conversation:session:"; - private static final String PHONE_TO_SESSION_KEY_PREFIX = "conversation:phone_to_session:"; private final ReactiveRedisTemplate redisTemplate; - private final ReactiveRedisTemplate stringRedisTemplate; + private final MemoryStoreConversationService memoryStoreConversationService; private final FirestoreConversationService firestoreConversationService; private final MemoryStoreNotificationService memoryStoreNotificationService; private final FirestoreNotificationService firestoreNotificationService; @@ -32,12 +32,12 @@ public class SessionPurgeService { @Autowired public SessionPurgeService( ReactiveRedisTemplate redisTemplate, - ReactiveRedisTemplate stringRedisTemplate, + MemoryStoreConversationService memoryStoreConversationService, FirestoreConversationService firestoreConversationService, MemoryStoreNotificationService memoryStoreNotificationService, FirestoreNotificationService firestoreNotificationService) { this.redisTemplate = redisTemplate; - this.stringRedisTemplate = stringRedisTemplate; + this.memoryStoreConversationService = memoryStoreConversationService; this.firestoreConversationService = firestoreConversationService; this.memoryStoreNotificationService = memoryStoreNotificationService; this.firestoreNotificationService = firestoreNotificationService; @@ -50,14 +50,12 @@ public class SessionPurgeService { return redisTemplate.opsForValue().get(sessionKey) .flatMap(session -> { if (session != null && session.telefono() != null) { - String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + session.telefono(); - return redisTemplate.opsForValue().delete(sessionKey) - .then(stringRedisTemplate.opsForValue().delete(phoneToSessionKey)) + return memoryStoreConversationService.deleteSession(sessionId) .then(firestoreConversationService.deleteSession(sessionId)) .then(memoryStoreNotificationService.deleteNotificationSession(session.telefono())) .then(firestoreNotificationService.deleteNotification(session.telefono())); } else { - return redisTemplate.opsForValue().delete(sessionKey) + return memoryStoreConversationService.deleteSession(sessionId) .then(firestoreConversationService.deleteSession(sessionId)); } }); diff --git a/src/main/java/com/example/service/conversation/FirestoreConversationService.java b/src/main/java/com/example/service/conversation/FirestoreConversationService.java index 70753c3..91e352a 100644 --- a/src/main/java/com/example/service/conversation/FirestoreConversationService.java +++ b/src/main/java/com/example/service/conversation/FirestoreConversationService.java @@ -28,7 +28,7 @@ public class FirestoreConversationService { private static final Logger logger = LoggerFactory.getLogger(FirestoreConversationService.class); private static final String CONVERSATION_COLLECTION_PATH_FORMAT = "artifacts/%s/conversations"; - private static final String MESSAGES_SUBCOLLECTION = "messages"; + private static final String MESSAGES_SUBCOLLECTION = "mensajes"; private final FirestoreBaseRepository firestoreBaseRepository; private final FirestoreConversationMapper firestoreConversationMapper; private final ConversationMessageMapper conversationMessageMapper; @@ -110,7 +110,7 @@ public class FirestoreConversationService { return Mono.fromRunnable(() -> { DocumentReference sessionDocRef = getSessionDocumentReference(sessionId); try { - firestoreBaseRepository.deleteDocument(sessionDocRef); + firestoreBaseRepository.deleteDocumentAndSubcollections(sessionDocRef, MESSAGES_SUBCOLLECTION); logger.info("Successfully deleted conversation session for session {}.", sessionId); } catch (InterruptedException | ExecutionException e) { handleException(e, sessionId); diff --git a/src/main/java/com/example/service/conversation/MemoryStoreConversationService.java b/src/main/java/com/example/service/conversation/MemoryStoreConversationService.java index 2948364..ae57eef 100644 --- a/src/main/java/com/example/service/conversation/MemoryStoreConversationService.java +++ b/src/main/java/com/example/service/conversation/MemoryStoreConversationService.java @@ -84,7 +84,18 @@ public class MemoryStoreConversationService { String sessionKey = SESSION_KEY_PREFIX + sessionId; String messagesKey = MESSAGES_KEY_PREFIX + sessionId; logger.info("Deleting session {} from Memorystore.", sessionId); - return redisTemplate.opsForValue().delete(sessionKey) - .then(messageRedisTemplate.opsForList().delete(messagesKey)).then(); + + return redisTemplate.opsForValue().get(sessionKey) + .flatMap(session -> { + if (session != null && session.telefono() != null) { + String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + session.telefono(); + return redisTemplate.opsForValue().delete(sessionKey) + .then(stringRedisTemplate.opsForValue().delete(phoneToSessionKey)) + .then(messageRedisTemplate.opsForList().delete(messagesKey)); + } else { + return redisTemplate.opsForValue().delete(sessionKey) + .then(messageRedisTemplate.opsForList().delete(messagesKey)); + } + }).then(); } } \ No newline at end of file