UPDATE code 20-Ago
This commit is contained in:
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -5,29 +5,13 @@
|
|||||||
|
|
||||||
package com.example;
|
package com.example;
|
||||||
|
|
||||||
import com.google.api.gax.core.CredentialsProvider;
|
|
||||||
import com.google.api.gax.core.NoCredentialsProvider;
|
|
||||||
import com.google.cloud.spring.data.firestore.repository.config.EnableReactiveFirestoreRepositories;
|
import com.google.cloud.spring.data.firestore.repository.config.EnableReactiveFirestoreRepositories;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|
||||||
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableTransactionManagement
|
|
||||||
@EnableReactiveFirestoreRepositories(basePackages = "com.example.repository")
|
@EnableReactiveFirestoreRepositories(basePackages = "com.example.repository")
|
||||||
public class Orchestrator {
|
public class Orchestrator {
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
value = "spring.cloud.gcp.firestore.emulator.enabled",
|
|
||||||
havingValue = "true")
|
|
||||||
public CredentialsProvider googleCredentials() {
|
|
||||||
return NoCredentialsProvider.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Orchestrator.class, args);
|
SpringApplication.run(Orchestrator.class, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||||
|
import org.springframework.data.redis.listener.PatternTopic;
|
||||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
import com.example.service.notification.NotificationExpirationListener;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring configuration class for setting up Reactive Redis(Memorystore in GCP)
|
* Spring configuration class for setting up Reactive Redis(Memorystore in GCP)
|
||||||
@@ -74,4 +79,14 @@ public ReactiveRedisTemplate<String, NotificationSessionDTO> reactiveNotificatio
|
|||||||
.build();
|
.build();
|
||||||
return new ReactiveRedisTemplate<>(factory, context);
|
return new ReactiveRedisTemplate<>(factory, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisMessageListenerContainer keyExpirationListenerContainer(
|
||||||
|
@Autowired RedisConnectionFactory connectionFactory,
|
||||||
|
@Autowired NotificationExpirationListener notificationExpirationListener) {
|
||||||
|
RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();
|
||||||
|
listenerContainer.setConnectionFactory(connectionFactory);
|
||||||
|
listenerContainer.addMessageListener(notificationExpirationListener, new PatternTopic("__keyevent@*__:expired"));
|
||||||
|
return listenerContainer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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.controller;
|
||||||
|
|
||||||
|
import com.example.dto.quickreplies.QuickReplyScreenRequestDTO;
|
||||||
|
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/quick-replies")
|
||||||
|
public class QuickRepliesController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(QuickRepliesController.class);
|
||||||
|
private final QuickRepliesManagerService quickRepliesManagerService;
|
||||||
|
|
||||||
|
public QuickRepliesController(QuickRepliesManagerService quickRepliesManagerService) {
|
||||||
|
this.quickRepliesManagerService = quickRepliesManagerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/screen")
|
||||||
|
public Mono<Map<String, String>> startSessionAndGetReplies(@Valid @RequestBody QuickReplyScreenRequestDTO request) {
|
||||||
|
return quickRepliesManagerService.startQuickReplySession(request)
|
||||||
|
.map(response -> Map.of("responseId", response.responseId()))
|
||||||
|
.doOnSuccess(response -> logger.info("Successfully processed quick reply request"))
|
||||||
|
.doOnError(error -> logger.error("Error processing quick reply request: {}", error.getMessage(), error));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,16 @@ package com.example.dto.dialogflow.base;
|
|||||||
import com.example.dto.dialogflow.conversation.QueryResultDTO;
|
import com.example.dto.dialogflow.conversation.QueryResultDTO;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import com.example.dto.quickreplies.QuickReplyDTO;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public record DetectIntentResponseDTO(
|
public record DetectIntentResponseDTO(
|
||||||
@JsonProperty("responseId") String responseId,
|
@JsonProperty("responseId") String responseId,
|
||||||
@JsonProperty("queryResult") QueryResultDTO queryResult
|
@JsonProperty("queryResult") QueryResultDTO queryResult,
|
||||||
) {}
|
@JsonProperty("quick_replies") QuickReplyDTO quickReplies
|
||||||
|
) {
|
||||||
|
public DetectIntentResponseDTO(String responseId, QueryResultDTO queryResult) {
|
||||||
|
this(responseId, queryResult, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,13 +30,25 @@ public record ConversationEntryDTO(
|
|||||||
|
|
||||||
public static ConversationEntryDTO forAgent(QueryResultDTO agentQueryResult) {
|
public static ConversationEntryDTO forAgent(QueryResultDTO agentQueryResult) {
|
||||||
String fulfillmentText = (agentQueryResult != null && agentQueryResult.responseText() != null) ? agentQueryResult.responseText() : "";
|
String fulfillmentText = (agentQueryResult != null && agentQueryResult.responseText() != null) ? agentQueryResult.responseText() : "";
|
||||||
|
Map<String, Object> parameters = (agentQueryResult != null) ? agentQueryResult.parameters() : null;
|
||||||
|
|
||||||
return new ConversationEntryDTO(
|
return new ConversationEntryDTO(
|
||||||
ConversationEntryEntity.AGENTE,
|
ConversationEntryEntity.AGENTE,
|
||||||
ConversationEntryType.CONVERSACION,
|
ConversationEntryType.CONVERSACION,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
fulfillmentText,
|
fulfillmentText,
|
||||||
agentQueryResult.parameters(),
|
parameters,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConversationEntryDTO forAgentWithMessage(String text) {
|
||||||
|
return new ConversationEntryDTO(
|
||||||
|
ConversationEntryEntity.AGENTE,
|
||||||
|
ConversationEntryType.CONVERSACION,
|
||||||
|
Instant.now(),
|
||||||
|
text,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,44 +6,53 @@
|
|||||||
package com.example.dto.dialogflow.conversation;
|
package com.example.dto.dialogflow.conversation;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
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.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@JsonInclude(Include.NON_NULL)
|
||||||
public record ConversationSessionDTO(
|
public record ConversationSessionDTO(
|
||||||
String sessionId,
|
String sessionId,
|
||||||
String userId,
|
String userId,
|
||||||
String telefono,
|
String telefono,
|
||||||
Instant createdAt,
|
Instant createdAt,
|
||||||
Instant lastModified,
|
Instant lastModified,
|
||||||
List<ConversationEntryDTO> entries
|
List<ConversationEntryDTO> entries,
|
||||||
|
String pantallaContexto
|
||||||
) {
|
) {
|
||||||
public ConversationSessionDTO(String sessionId, String userId, String telefono, Instant createdAt, Instant lastModified, List<ConversationEntryDTO> entries) {
|
public ConversationSessionDTO(String sessionId, String userId, String telefono, Instant createdAt, Instant lastModified, List<ConversationEntryDTO> entries, String pantallaContexto) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.telefono = telefono;
|
this.telefono = telefono;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.lastModified = lastModified;
|
this.lastModified = lastModified;
|
||||||
this.entries = Collections.unmodifiableList(new ArrayList<>(entries));
|
this.entries = Collections.unmodifiableList(new ArrayList<>(entries));
|
||||||
|
this.pantallaContexto = pantallaContexto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ConversationSessionDTO create(String sessionId, String userId, String telefono) {
|
public static ConversationSessionDTO create(String sessionId, String userId, String telefono) {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
return new ConversationSessionDTO(sessionId, userId, telefono, now, now, Collections.emptyList());
|
return new ConversationSessionDTO(sessionId, userId, telefono, now, now, Collections.emptyList(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConversationSessionDTO withAddedEntry(ConversationEntryDTO newEntry) {
|
public ConversationSessionDTO withAddedEntry(ConversationEntryDTO newEntry) {
|
||||||
List<ConversationEntryDTO> updatedEntries = new ArrayList<>(this.entries);
|
List<ConversationEntryDTO> updatedEntries = new ArrayList<>(this.entries);
|
||||||
updatedEntries.add(newEntry);
|
updatedEntries.add(newEntry);
|
||||||
return new ConversationSessionDTO(this.sessionId, this.userId, this.telefono, this.createdAt, Instant.now(), updatedEntries);
|
return new ConversationSessionDTO(this.sessionId, this.userId, this.telefono, this.createdAt, Instant.now(), updatedEntries, this.pantallaContexto);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConversationSessionDTO withTelefono(String newTelefono) {
|
public ConversationSessionDTO withTelefono(String newTelefono) {
|
||||||
if (newTelefono != null && !newTelefono.equals(this.telefono)) {
|
if (newTelefono != null && !newTelefono.equals(this.telefono)) {
|
||||||
return new ConversationSessionDTO(this.sessionId, this.userId, newTelefono, this.createdAt, this.lastModified, this.entries);
|
return new ConversationSessionDTO(this.sessionId, this.userId, newTelefono, this.createdAt, this.lastModified, this.entries, this.pantallaContexto);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ConversationSessionDTO withPantallaContexto(String pantallaContexto) {
|
||||||
|
return new ConversationSessionDTO(this.sessionId, this.userId, this.telefono, this.createdAt, this.lastModified, this.entries, pantallaContexto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,8 @@ public record ExternalConvRequestDTO(
|
|||||||
@JsonProperty("mensaje") String message,
|
@JsonProperty("mensaje") String message,
|
||||||
@JsonProperty("usuario") UsuarioDTO user,
|
@JsonProperty("usuario") UsuarioDTO user,
|
||||||
@JsonProperty("canal") String channel,
|
@JsonProperty("canal") String channel,
|
||||||
@JsonProperty("tipo") ConversationEntryType tipo
|
@JsonProperty("tipo") ConversationEntryType tipo,
|
||||||
|
@JsonProperty("pantallaContexto") String pantallaContexto //optional field for quick-replies
|
||||||
) {
|
) {
|
||||||
public ExternalConvRequestDTO {}
|
public ExternalConvRequestDTO {}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@ public record NotificationDTO(
|
|||||||
String texto, // 'texto' original de NotificationRequestDTO (si aplica)
|
String texto, // 'texto' original de NotificationRequestDTO (si aplica)
|
||||||
String nombreEventoDialogflow, // Nombre del evento enviado a Dialogflow (ej. "tu Estado de cuenta listo")
|
String nombreEventoDialogflow, // Nombre del evento enviado a Dialogflow (ej. "tu Estado de cuenta listo")
|
||||||
String codigoIdiomaDialogflow, // Código de idioma usado para el evento
|
String codigoIdiomaDialogflow, // Código de idioma usado para el evento
|
||||||
Map<String, Object> parametros // Parámetros de sesión finales después del procesamiento de// Dialogflow
|
Map<String, Object> parametros, // Parámetros de sesión finales después del procesamiento de// Dialogflow
|
||||||
|
String status
|
||||||
) {
|
) {
|
||||||
public NotificationDTO {
|
public NotificationDTO {
|
||||||
Objects.requireNonNull(idNotificacion, "Notification ID cannot be null.");
|
Objects.requireNonNull(idNotificacion, "Notification ID cannot be null.");
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.example.dto.quickreplies;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record QuestionDTO(
|
||||||
|
@JsonProperty("titulo") String titulo,
|
||||||
|
@JsonProperty("descripcion") String descripcion
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.example.dto.quickreplies;
|
||||||
|
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationEntryType;
|
||||||
|
import com.example.dto.dialogflow.conversation.UsuarioDTO;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record QuickReplyScreenRequestDTO(
|
||||||
|
@JsonProperty("usuario") UsuarioDTO user,
|
||||||
|
@JsonProperty("canal") String channel,
|
||||||
|
@JsonProperty("tipo") ConversationEntryType tipo,
|
||||||
|
@JsonProperty("pantallaContexto") String pantallaContexto
|
||||||
|
) {}
|
||||||
@@ -76,15 +76,14 @@ public class DialogflowRequestMapper {
|
|||||||
for (Map.Entry<String, Object> entry : requestDto.queryParams().parameters().entrySet()) {
|
for (Map.Entry<String, Object> entry : requestDto.queryParams().parameters().entrySet()) {
|
||||||
Value protobufValue = ProtobufUtil.convertJavaObjectToProtobufValue(entry.getValue());
|
Value protobufValue = ProtobufUtil.convertJavaObjectToProtobufValue(entry.getValue());
|
||||||
paramsStructBuilder.putFields(entry.getKey(), protobufValue);
|
paramsStructBuilder.putFields(entry.getKey(), protobufValue);
|
||||||
logger.info("Added session parameter from DTO queryParams: Key='{}', Value='{}'", entry.getKey(),
|
logger.debug("Added session parameter from DTO queryParams: Key='{}', Value='{}'",
|
||||||
entry.getValue());
|
entry.getKey(),entry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paramsStructBuilder.getFieldsCount() > 0) {
|
if (paramsStructBuilder.getFieldsCount() > 0) {
|
||||||
queryParametersBuilder.setParameters(paramsStructBuilder.build());
|
queryParametersBuilder.setParameters(paramsStructBuilder.build());
|
||||||
logger.debug(
|
logger.debug("All custom session parameters added to Protobuf request builder.");
|
||||||
"All custom session parameters (including telefono and canal if present) added to Protobuf request builder.");
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug("No custom session parameters to add to Protobuf request.");
|
logger.debug("No custom session parameters to add to Protobuf request.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class FirestoreConversationMapper {
|
|||||||
private static final String FIELD_CREATED_AT = "fechaCreacion";
|
private static final String FIELD_CREATED_AT = "fechaCreacion";
|
||||||
private static final String FIELD_LAST_UPDATED = "ultimaActualizacion";
|
private static final String FIELD_LAST_UPDATED = "ultimaActualizacion";
|
||||||
private static final String FIELD_MESSAGES = "mensajes";
|
private static final String FIELD_MESSAGES = "mensajes";
|
||||||
|
private static final String FIELD_PANTALLA_CONTEXTO = "pantallaContexto";
|
||||||
|
|
||||||
// Constants for fields within the 'mensajes' sub-documents
|
// Constants for fields within the 'mensajes' sub-documents
|
||||||
private static final String FIELD_MESSAGE_ENTITY = "entidad";
|
private static final String FIELD_MESSAGE_ENTITY = "entidad";
|
||||||
@@ -56,6 +57,10 @@ public class FirestoreConversationMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> createNewSessionMapForSingleEntry(String sessionId, String userId, String telefono, ConversationEntryDTO initialEntry) {
|
public Map<String, Object> createNewSessionMapForSingleEntry(String sessionId, String userId, String telefono, ConversationEntryDTO initialEntry) {
|
||||||
|
return createNewSessionMapForSingleEntry(sessionId, userId, telefono, initialEntry, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> createNewSessionMapForSingleEntry(String sessionId, String userId, String telefono, ConversationEntryDTO initialEntry, String pantallaContexto) {
|
||||||
Map<String, Object> sessionMap = new HashMap<>();
|
Map<String, Object> sessionMap = new HashMap<>();
|
||||||
sessionMap.put(FIELD_SESSION_ID, sessionId);
|
sessionMap.put(FIELD_SESSION_ID, sessionId);
|
||||||
sessionMap.put(FIELD_USER_ID, userId);
|
sessionMap.put(FIELD_USER_ID, userId);
|
||||||
@@ -66,6 +71,10 @@ public class FirestoreConversationMapper {
|
|||||||
sessionMap.put(FIELD_PHONE_NUMBER, null);
|
sessionMap.put(FIELD_PHONE_NUMBER, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pantallaContexto != null) {
|
||||||
|
sessionMap.put(FIELD_PANTALLA_CONTEXTO, pantallaContexto);
|
||||||
|
}
|
||||||
|
|
||||||
sessionMap.put(FIELD_CREATED_AT, Timestamp.of(java.util.Date.from(Instant.now())));
|
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())));
|
sessionMap.put(FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())));
|
||||||
|
|
||||||
@@ -100,6 +109,7 @@ public class FirestoreConversationMapper {
|
|||||||
String sessionId = documentSnapshot.getString(FIELD_SESSION_ID);
|
String sessionId = documentSnapshot.getString(FIELD_SESSION_ID);
|
||||||
String userId = documentSnapshot.getString(FIELD_USER_ID);
|
String userId = documentSnapshot.getString(FIELD_USER_ID);
|
||||||
String telefono = documentSnapshot.getString(FIELD_PHONE_NUMBER);
|
String telefono = documentSnapshot.getString(FIELD_PHONE_NUMBER);
|
||||||
|
String pantallaContexto = documentSnapshot.getString(FIELD_PANTALLA_CONTEXTO);
|
||||||
|
|
||||||
Timestamp createdAtFirestore = documentSnapshot.getTimestamp(FIELD_CREATED_AT);
|
Timestamp createdAtFirestore = documentSnapshot.getTimestamp(FIELD_CREATED_AT);
|
||||||
Instant createdAt = (createdAtFirestore != null) ? createdAtFirestore.toDate().toInstant() : null;
|
Instant createdAt = (createdAtFirestore != null) ? createdAtFirestore.toDate().toInstant() : null;
|
||||||
@@ -115,7 +125,7 @@ public class FirestoreConversationMapper {
|
|||||||
.map(this::mapFirestoreEntryMapToConversationEntryDTO)
|
.map(this::mapFirestoreEntryMapToConversationEntryDTO)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
return new ConversationSessionDTO(sessionId, userId, telefono, createdAt, lastModified, entries);
|
return new ConversationSessionDTO(sessionId, userId, telefono, createdAt, lastModified, entries, pantallaContexto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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.ConversationEntryDTO;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
|
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 MESSAGE_LIMIT = 60;
|
||||||
|
private static final int DAYS_LIMIT = 30;
|
||||||
|
|
||||||
|
public String toText(ConversationSessionDTO session) {
|
||||||
|
if (session == null || session.entries() == null || session.entries().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.entries().stream()
|
||||||
|
.map(this::formatEntry)
|
||||||
|
.collect(Collectors.joining(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toTextWithLimits(ConversationSessionDTO session) {
|
||||||
|
if (session == null || session.entries() == null || session.entries().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant thirtyDaysAgo = Instant.now().minus(DAYS_LIMIT, ChronoUnit.DAYS);
|
||||||
|
|
||||||
|
List<ConversationEntryDTO> recentEntries = session.entries().stream()
|
||||||
|
.filter(entry -> entry.timestamp().isAfter(thirtyDaysAgo))
|
||||||
|
.sorted(Comparator.comparing(ConversationEntryDTO::timestamp).reversed())
|
||||||
|
.limit(MESSAGE_LIMIT)
|
||||||
|
.sorted(Comparator.comparing(ConversationEntryDTO::timestamp))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return recentEntries.stream()
|
||||||
|
.map(this::formatEntry)
|
||||||
|
.collect(Collectors.joining(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatEntry(ConversationEntryDTO entry) {
|
||||||
|
String prefix = "User: ";
|
||||||
|
if (entry.entity() != null) {
|
||||||
|
switch (entry.entity()) {
|
||||||
|
case AGENTE:
|
||||||
|
prefix = "Agent: ";
|
||||||
|
break;
|
||||||
|
case USUARIO:
|
||||||
|
default:
|
||||||
|
prefix = "User: ";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix + entry.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.notification.NotificationDTO;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class NotificationContextMapper {
|
||||||
|
|
||||||
|
public String toText(NotificationDTO notification) {
|
||||||
|
if (notification == null || notification.texto() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return notification.texto();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toText(List<NotificationDTO> notifications) {
|
||||||
|
if (notifications == null || notifications.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return notifications.stream()
|
||||||
|
.map(NotificationDTO::texto)
|
||||||
|
.filter(texto -> texto != null && !texto.isBlank())
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
// src/main/java/com/example/mapper/notification/FirestoreNotificationMapper.java
|
/*
|
||||||
|
* 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.notification;
|
package com.example.mapper.notification;
|
||||||
|
|
||||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||||
@@ -22,6 +25,7 @@ public class FirestoreNotificationMapper {
|
|||||||
private static final String DEFAULT_LANGUAGE_CODE = "es";
|
private static final String DEFAULT_LANGUAGE_CODE = "es";
|
||||||
private static final String FIXED_EVENT_NAME = "notificacion";
|
private static final String FIXED_EVENT_NAME = "notificacion";
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private static final String DEFAULT_NOTIFICATION_STATUS="ACTIVE";
|
||||||
|
|
||||||
public FirestoreNotificationMapper(ObjectMapper objectMapper) {
|
public FirestoreNotificationMapper(ObjectMapper objectMapper) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
@@ -44,7 +48,8 @@ public class FirestoreNotificationMapper {
|
|||||||
notificationText,
|
notificationText,
|
||||||
FIXED_EVENT_NAME,
|
FIXED_EVENT_NAME,
|
||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
parameters
|
parameters,
|
||||||
|
DEFAULT_NOTIFICATION_STATUS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package com.example.repository;
|
package com.example.repository;
|
||||||
|
|
||||||
|
import com.example.util.FirestoreTimestampDeserializer;
|
||||||
|
import com.example.util.FirestoreTimestampSerializer;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
@@ -14,26 +16,24 @@ import com.google.api.core.ApiFuture;
|
|||||||
import com.google.cloud.firestore.DocumentReference;
|
import com.google.cloud.firestore.DocumentReference;
|
||||||
import com.google.cloud.firestore.DocumentSnapshot;
|
import com.google.cloud.firestore.DocumentSnapshot;
|
||||||
import com.google.cloud.firestore.Firestore;
|
import com.google.cloud.firestore.Firestore;
|
||||||
|
import com.google.cloud.firestore.Query;
|
||||||
|
import com.google.cloud.firestore.QuerySnapshot;
|
||||||
import com.google.cloud.firestore.WriteBatch;
|
import com.google.cloud.firestore.WriteBatch;
|
||||||
import com.google.cloud.firestore.WriteResult;
|
import com.google.cloud.firestore.WriteResult;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import com.example.util.FirestoreTimestampDeserializer;
|
|
||||||
import com.example.util.FirestoreTimestampSerializer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base repository for performing low-level operations with Firestore.
|
* A base repository for performing low-level operations with Firestore. It provides a generic
|
||||||
* It provides a generic interface for common data access tasks such as
|
* interface for common data access tasks such as getting document references, performing reads,
|
||||||
* getting document references, performing reads, writes, and batched updates.
|
* writes, and batched updates. This class also handles the serialization and deserialization of
|
||||||
* This class also handles the serialization and deserialization of Java objects
|
* Java objects to and from Firestore documents using an `ObjectMapper`.
|
||||||
* to and from Firestore documents using an `ObjectMapper`.
|
|
||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public class FirestoreBaseRepository {
|
public class FirestoreBaseRepository {
|
||||||
@@ -66,11 +66,15 @@ public class FirestoreBaseRepository {
|
|||||||
// They are generally not the cause of the Redis deserialization error for Instant.
|
// They are generally not the cause of the Redis deserialization error for Instant.
|
||||||
|
|
||||||
SimpleModule firestoreTimestampModule = new SimpleModule();
|
SimpleModule firestoreTimestampModule = new SimpleModule();
|
||||||
firestoreTimestampModule.addDeserializer(com.google.cloud.Timestamp.class, new FirestoreTimestampDeserializer());
|
firestoreTimestampModule.addDeserializer(
|
||||||
firestoreTimestampModule.addSerializer(com.google.cloud.Timestamp.class, new FirestoreTimestampSerializer());
|
com.google.cloud.Timestamp.class, new FirestoreTimestampDeserializer());
|
||||||
|
firestoreTimestampModule.addSerializer(
|
||||||
|
com.google.cloud.Timestamp.class, new FirestoreTimestampSerializer());
|
||||||
objectMapper.registerModule(firestoreTimestampModule);
|
objectMapper.registerModule(firestoreTimestampModule);
|
||||||
|
|
||||||
logger.info("FirestoreBaseRepository initialized with Firestore client and ObjectMapper. App ID will be: {}", appId);
|
logger.info(
|
||||||
|
"FirestoreBaseRepository initialized with Firestore client and ObjectMapper. App ID will be: {}",
|
||||||
|
appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DocumentReference getDocumentReference(String collectionPath, String documentId) {
|
public DocumentReference getDocumentReference(String collectionPath, String documentId) {
|
||||||
@@ -79,50 +83,76 @@ public class FirestoreBaseRepository {
|
|||||||
return firestore.collection(collectionPath).document(documentId);
|
return firestore.collection(collectionPath).document(documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T getDocument(DocumentReference docRef, Class<T> clazz) throws InterruptedException, ExecutionException {
|
public <T> T getDocument(DocumentReference docRef, Class<T> clazz)
|
||||||
|
throws InterruptedException, ExecutionException {
|
||||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||||
Objects.requireNonNull(clazz, "Class for mapping cannot be null.");
|
Objects.requireNonNull(clazz, "Class for mapping cannot be null.");
|
||||||
ApiFuture<DocumentSnapshot> future = docRef.get();
|
ApiFuture<DocumentSnapshot> future = docRef.get();
|
||||||
DocumentSnapshot document = future.get();
|
DocumentSnapshot document = future.get();
|
||||||
if (document.exists()) {
|
if (document.exists()) {
|
||||||
try {
|
try {
|
||||||
logger.debug("FirestoreBaseRepository: Raw document data for {}: {}", docRef.getPath(), document.getData());
|
logger.debug(
|
||||||
|
"FirestoreBaseRepository: Raw document data for {}: {}",
|
||||||
|
docRef.getPath(),
|
||||||
|
document.getData());
|
||||||
T result = objectMapper.convertValue(document.getData(), clazz);
|
T result = objectMapper.convertValue(document.getData(), clazz);
|
||||||
return result;
|
return result;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
logger.error("Failed to convert Firestore document data to {}: {}", clazz.getName(), e.getMessage(), e);
|
logger.error(
|
||||||
throw new RuntimeException("Failed to convert Firestore document data to " + clazz.getName(), e);
|
"Failed to convert Firestore document data to {}: {}", clazz.getName(), e.getMessage(), e);
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to convert Firestore document data to " + clazz.getName(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DocumentSnapshot getDocumentSnapshot(DocumentReference docRef) throws ExecutionException, InterruptedException {
|
public DocumentSnapshot getDocumentSnapshot(DocumentReference docRef)
|
||||||
|
throws ExecutionException, InterruptedException {
|
||||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||||
ApiFuture<DocumentSnapshot> future = docRef.get();
|
ApiFuture<DocumentSnapshot> future = docRef.get();
|
||||||
return future.get();
|
return future.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean documentExists(DocumentReference docRef) throws InterruptedException, ExecutionException {
|
public Mono<DocumentSnapshot> getDocumentsByField(
|
||||||
|
String collectionPath, String fieldName, String value) {
|
||||||
|
return Mono.fromCallable(
|
||||||
|
() -> {
|
||||||
|
Query query = firestore.collection(collectionPath).whereEqualTo(fieldName, value);
|
||||||
|
ApiFuture<QuerySnapshot> future = query.get();
|
||||||
|
QuerySnapshot querySnapshot = future.get();
|
||||||
|
if (!querySnapshot.isEmpty()) {
|
||||||
|
return querySnapshot.getDocuments().get(0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean documentExists(DocumentReference docRef)
|
||||||
|
throws InterruptedException, ExecutionException {
|
||||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||||
ApiFuture<DocumentSnapshot> future = docRef.get();
|
ApiFuture<DocumentSnapshot> future = docRef.get();
|
||||||
return future.get().exists();
|
return future.get().exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDocument(DocumentReference docRef, Object data) throws InterruptedException, ExecutionException {
|
public void setDocument(DocumentReference docRef, Object data)
|
||||||
|
throws InterruptedException, ExecutionException {
|
||||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||||
Objects.requireNonNull(data, "Data for setting document cannot be null.");
|
Objects.requireNonNull(data, "Data for setting document cannot be null.");
|
||||||
ApiFuture<WriteResult> future = docRef.set(data);
|
ApiFuture<WriteResult> future = docRef.set(data);
|
||||||
WriteResult writeResult = future.get();
|
WriteResult writeResult = future.get();
|
||||||
logger.debug("Document set: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
|
logger.debug(
|
||||||
|
"Document set: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateDocument(DocumentReference docRef, Map<String, Object> updates) throws InterruptedException, ExecutionException {
|
public void updateDocument(DocumentReference docRef, Map<String, Object> updates)
|
||||||
|
throws InterruptedException, ExecutionException {
|
||||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||||
Objects.requireNonNull(updates, "Updates map cannot be null.");
|
Objects.requireNonNull(updates, "Updates map cannot be null.");
|
||||||
ApiFuture<WriteResult> future = docRef.update(updates);
|
ApiFuture<WriteResult> future = docRef.update(updates);
|
||||||
WriteResult writeResult = future.get();
|
WriteResult writeResult = future.get();
|
||||||
logger.debug("Document updated: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
|
logger.debug(
|
||||||
|
"Document updated: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
public WriteBatch createBatch() {
|
public WriteBatch createBatch() {
|
||||||
@@ -138,5 +168,4 @@ public class FirestoreBaseRepository {
|
|||||||
public String getAppId() {
|
public String getAppId() {
|
||||||
return appId;
|
return appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,133 +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.repository;
|
|
||||||
|
|
||||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
|
||||||
import com.example.util.FirestoreTimestampDeserializer;
|
|
||||||
import com.example.util.FirestoreTimestampSerializer;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
|
||||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
|
||||||
|
|
||||||
import com.google.cloud.firestore.Firestore;
|
|
||||||
import com.google.cloud.firestore.DocumentReference;
|
|
||||||
import com.google.cloud.firestore.DocumentSnapshot;
|
|
||||||
import com.google.cloud.firestore.SetOptions;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import reactor.core.scheduler.Schedulers;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository for managing `NotificationDTO` objects in Firestore.
|
|
||||||
* It provides reactive methods for saving and retrieving notification data,
|
|
||||||
* handling the serialization and deserialization of the DTOs and managing
|
|
||||||
* the document paths within a structured collection.
|
|
||||||
*/
|
|
||||||
@Repository
|
|
||||||
public class FirestoreNotificationRepository {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(FirestoreNotificationRepository.class);
|
|
||||||
|
|
||||||
private final Firestore firestore;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Value("${app.id:default-app-id}")
|
|
||||||
private String appId;
|
|
||||||
|
|
||||||
private final String BASE_COLLECTION_PATH_FORMAT = "artifacts/%s/notifications";
|
|
||||||
|
|
||||||
public FirestoreNotificationRepository(Firestore firestore, ObjectMapper objectMapper) {
|
|
||||||
this.firestore = firestore;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
|
|
||||||
// Ensure ObjectMapper is configured for Java Time and Records, and Firestore
|
|
||||||
// Timestamps
|
|
||||||
if (!ObjectMapper.findModules().stream().anyMatch(m -> m instanceof JavaTimeModule)) {
|
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
|
||||||
}
|
|
||||||
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
|
||||||
|
|
||||||
if (!ObjectMapper.findModules().stream().anyMatch(m -> m instanceof ParameterNamesModule)) {
|
|
||||||
objectMapper.registerModule(new ParameterNamesModule());
|
|
||||||
}
|
|
||||||
|
|
||||||
// These specific Timestamp (Google Cloud) deserializers/serializers are for ObjectMapper
|
|
||||||
// to handle com.google.cloud.Timestamp objects when mapping other types.
|
|
||||||
// They are generally not the cause of the Redis deserialization error for Instant.
|
|
||||||
|
|
||||||
SimpleModule firestoreTimestampModule = new SimpleModule();
|
|
||||||
firestoreTimestampModule.addDeserializer(com.google.cloud.Timestamp.class,
|
|
||||||
new FirestoreTimestampDeserializer());
|
|
||||||
firestoreTimestampModule.addSerializer(com.google.cloud.Timestamp.class, new FirestoreTimestampSerializer());
|
|
||||||
objectMapper.registerModule(firestoreTimestampModule);
|
|
||||||
|
|
||||||
logger.info("FirestoreNotificationRepository initialized for project: {}", appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DocumentReference getDocumentReference(String notificationId) {
|
|
||||||
String collectionPath = String.format(BASE_COLLECTION_PATH_FORMAT, appId);
|
|
||||||
return firestore.collection(collectionPath).document(notificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Mono<Void> saveNotification(NotificationDTO notification) {
|
|
||||||
Objects.requireNonNull(notification.idNotificacion(), "Notification ID cannot be null.");
|
|
||||||
|
|
||||||
return Mono.fromCallable((Callable<Void>) () -> {
|
|
||||||
DocumentReference docRef = getDocumentReference(notification.idNotificacion());
|
|
||||||
logger.debug("Attempting to save notification with ID {} to Firestore path: {}",
|
|
||||||
notification.idNotificacion(), docRef.getPath());
|
|
||||||
|
|
||||||
docRef.set(notification, SetOptions.merge()).get();
|
|
||||||
logger.info("Notification with ID {} successfully saved to Firestore.", notification.idNotificacion());
|
|
||||||
return null;
|
|
||||||
}).subscribeOn(Schedulers.boundedElastic())
|
|
||||||
.doOnError(e -> logger.error("Failed to save notification with ID {} to Firestore: {}",
|
|
||||||
notification.idNotificacion(), e.getMessage(), e));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Mono<NotificationDTO> getNotification(String notificationId) { // Renamed method
|
|
||||||
Objects.requireNonNull(notificationId, "Notification ID cannot be null for retrieval.");
|
|
||||||
|
|
||||||
return Mono.fromCallable((Callable<NotificationDTO>) () -> {
|
|
||||||
DocumentReference docRef = getDocumentReference(notificationId);
|
|
||||||
|
|
||||||
logger.debug("Attempting to retrieve notification with ID {} from Firestore path: {}", notificationId,
|
|
||||||
docRef.getPath());
|
|
||||||
|
|
||||||
DocumentSnapshot document = docRef.get().get(); // Blocking call
|
|
||||||
if (document.exists()) {
|
|
||||||
try {
|
|
||||||
NotificationDTO notification = objectMapper.convertValue(document.getData(), NotificationDTO.class);
|
|
||||||
logger.info("Notification with ID {} successfully retrieved from Firestore.", notificationId);
|
|
||||||
return notification;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
logger.error(
|
|
||||||
"Failed to convert Firestore document data to Notification for ID {}: {}",
|
|
||||||
notificationId, e.getMessage(), e);
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to convert Firestore document data to Notification for ID "
|
|
||||||
+ notificationId,
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.debug("Notification with ID {} not found in Firestore.", notificationId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).subscribeOn(Schedulers.boundedElastic())
|
|
||||||
.doOnError(e -> logger.error("Failed to retrieve notification with ID {} from Firestore: {}",
|
|
||||||
notificationId, e.getMessage(), e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.example.service.base;
|
|
||||||
|
|
||||||
public class ConvSessionCloserService {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -5,68 +5,103 @@
|
|||||||
|
|
||||||
package com.example.service.conversation;
|
package com.example.service.conversation;
|
||||||
|
|
||||||
import com.example.mapper.conversation.ExternalConvRequestMapper;
|
|
||||||
import com.example.service.base.DialogflowClientService;
|
|
||||||
import com.example.util.SessionIdGenerator;
|
|
||||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||||
import com.example.dto.dialogflow.conversation.ConversationContext;
|
import com.example.dto.dialogflow.conversation.ConversationContext;
|
||||||
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
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.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
import com.example.dto.dialogflow.conversation.ExternalConvRequestDTO;
|
import com.example.dto.dialogflow.conversation.ExternalConvRequestDTO;
|
||||||
|
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||||
|
import com.example.mapper.conversation.ExternalConvRequestMapper;
|
||||||
|
import com.example.mapper.messagefilter.ConversationContextMapper;
|
||||||
|
import com.example.mapper.messagefilter.NotificationContextMapper;
|
||||||
|
import com.example.service.base.DialogflowClientService;
|
||||||
|
import com.example.service.base.MessageEntryFilter;
|
||||||
|
import com.example.service.notification.MemoryStoreNotificationService;
|
||||||
|
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||||
|
import com.example.util.SessionIdGenerator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for orchestrating the end-to-end conversation flow.
|
|
||||||
* It manages user sessions, creating new ones or reusing existing ones
|
|
||||||
* based on a session reset threshold. The service handles the entire
|
|
||||||
* conversation turn, from mapping an external request to calling Dialogflow,
|
|
||||||
* and then persists both user and agent messages using a write-back strategy
|
|
||||||
* to a primary cache (Redis) and an asynchronous write to Firestore.
|
|
||||||
*/
|
|
||||||
@Service
|
@Service
|
||||||
public class ConversationManagerService {
|
public class ConversationManagerService {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ConversationManagerService.class);
|
private static final Logger logger = LoggerFactory.getLogger(ConversationManagerService.class);
|
||||||
|
|
||||||
private static final long SESSION_RESET_THRESHOLD_HOURS = 24;
|
private static final long SESSION_RESET_THRESHOLD_HOURS = 24;
|
||||||
private static final String CURRENT_PAGE_PARAM = "currentPage";
|
private static final String CURRENT_PAGE_PARAM = "currentPage";
|
||||||
private final ExternalConvRequestMapper externalRequestToDialogflowMapper;
|
|
||||||
|
|
||||||
|
private final ExternalConvRequestMapper externalRequestToDialogflowMapper;
|
||||||
private final DialogflowClientService dialogflowServiceClient;
|
private final DialogflowClientService dialogflowServiceClient;
|
||||||
private final FirestoreConversationService firestoreConversationService;
|
private final FirestoreConversationService firestoreConversationService;
|
||||||
private final MemoryStoreConversationService memoryStoreConversationService;
|
private final MemoryStoreConversationService memoryStoreConversationService;
|
||||||
|
private final QuickRepliesManagerService quickRepliesManagerService;
|
||||||
|
private final MessageEntryFilter messageEntryFilter;
|
||||||
|
private final MemoryStoreNotificationService memoryStoreNotificationService;
|
||||||
|
private final NotificationContextMapper notificationContextMapper;
|
||||||
|
private final ConversationContextMapper conversationContextMapper;
|
||||||
|
|
||||||
public ConversationManagerService(
|
public ConversationManagerService(
|
||||||
DialogflowClientService dialogflowServiceClient,
|
DialogflowClientService dialogflowServiceClient,
|
||||||
FirestoreConversationService firestoreConversationService,
|
FirestoreConversationService firestoreConversationService,
|
||||||
MemoryStoreConversationService memoryStoreConversationService,
|
MemoryStoreConversationService memoryStoreConversationService,
|
||||||
ExternalConvRequestMapper externalRequestToDialogflowMapper) {
|
ExternalConvRequestMapper externalRequestToDialogflowMapper,
|
||||||
|
QuickRepliesManagerService quickRepliesManagerService,
|
||||||
|
MessageEntryFilter messageEntryFilter,
|
||||||
|
MemoryStoreNotificationService memoryStoreNotificationService,
|
||||||
|
NotificationContextMapper notificationContextMapper,
|
||||||
|
ConversationContextMapper conversationContextMapper) {
|
||||||
this.dialogflowServiceClient = dialogflowServiceClient;
|
this.dialogflowServiceClient = dialogflowServiceClient;
|
||||||
this.firestoreConversationService = firestoreConversationService;
|
this.firestoreConversationService = firestoreConversationService;
|
||||||
this.memoryStoreConversationService = memoryStoreConversationService;
|
this.memoryStoreConversationService = memoryStoreConversationService;
|
||||||
this.externalRequestToDialogflowMapper = externalRequestToDialogflowMapper;
|
this.externalRequestToDialogflowMapper = externalRequestToDialogflowMapper;
|
||||||
|
this.quickRepliesManagerService = quickRepliesManagerService;
|
||||||
|
this.messageEntryFilter = messageEntryFilter;
|
||||||
|
this.memoryStoreNotificationService = memoryStoreNotificationService;
|
||||||
|
this.notificationContextMapper = notificationContextMapper;
|
||||||
|
this.conversationContextMapper = conversationContextMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
externalrequest.user(),
|
||||||
|
externalrequest.channel(),
|
||||||
|
externalrequest.tipo(),
|
||||||
|
session.pantallaContexto()
|
||||||
|
);
|
||||||
|
return quickRepliesManagerService.manageConversation(updatedRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return continueManagingConversation(externalrequest);
|
||||||
|
})
|
||||||
|
.switchIfEmpty(continueManagingConversation(externalrequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<DetectIntentResponseDTO>manageConversation(ExternalConvRequestDTO Externalrequest) {
|
private Mono<DetectIntentResponseDTO> continueManagingConversation(ExternalConvRequestDTO externalrequest) {
|
||||||
final DetectIntentRequestDTO request;
|
final DetectIntentRequestDTO request;
|
||||||
try {
|
try {
|
||||||
request = externalRequestToDialogflowMapper.mapExternalRequestToDetectIntentRequest(Externalrequest);
|
request = externalRequestToDialogflowMapper.mapExternalRequestToDetectIntentRequest(externalrequest);
|
||||||
logger.debug("Successfully pre-mapped ExternalRequestDTO to DetectIntentRequestDTO");
|
logger.debug("Successfully pre-mapped ExternalRequestDTO to DetectIntentRequestDTO");
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
logger.error("Error during pre-mapping: {}", e.getMessage());
|
logger.error("Error during pre-mapping: {}", e.getMessage());
|
||||||
return Mono.error(new IllegalArgumentException("Failed to process external request due to mapping error: " + e.getMessage(), e));
|
return Mono.error(new IllegalArgumentException(
|
||||||
|
"Failed to process external request due to mapping error: " + e.getMessage(), e));
|
||||||
}
|
}
|
||||||
|
|
||||||
final ConversationContext context;
|
final ConversationContext context;
|
||||||
@@ -77,86 +112,144 @@ public class ConversationManagerService {
|
|||||||
return Mono.error(e);
|
return Mono.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
.filter(notification -> "active".equalsIgnoreCase(notification.status()))
|
||||||
|
.max(java.util.Comparator.comparing(NotificationDTO::timestampCreacion))
|
||||||
|
.orElse(null))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.flatMap((NotificationDTO notification) -> {
|
||||||
|
String notificationText = notificationContextMapper.toText(notification);
|
||||||
|
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||||
|
.map(conversationContextMapper::toText)
|
||||||
|
.defaultIfEmpty("")
|
||||||
|
.flatMap(conversationHistory -> {
|
||||||
|
String classification = messageEntryFilter.classifyMessage(userMessageText, notificationText, conversationHistory);
|
||||||
|
if (MessageEntryFilter.CATEGORY_NOTIFICATION.equals(classification)) {
|
||||||
|
return startNotificationConversation(context, request, notification);
|
||||||
|
} else {
|
||||||
|
return continueConversationFlow(context, request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.switchIfEmpty(continueConversationFlow(context, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<DetectIntentResponseDTO> continueConversationFlow(ConversationContext context, DetectIntentRequestDTO request) {
|
||||||
final String userId = context.userId();
|
final String userId = context.userId();
|
||||||
final String userMessageText = context.userMessageText();
|
final String userMessageText = context.userMessageText();
|
||||||
final String userPhoneNumber = context.primaryPhoneNumber();
|
final String userPhoneNumber = context.primaryPhoneNumber();
|
||||||
|
|
||||||
Mono<ConversationSessionDTO> sessionMono;
|
if (userPhoneNumber == null || userPhoneNumber.isBlank()) {
|
||||||
if (userPhoneNumber != null && !userPhoneNumber.isBlank()) {
|
|
||||||
logger.info("Checking for existing session for phone number: {}", userPhoneNumber);
|
|
||||||
sessionMono = memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
|
||||||
.doOnNext(session -> logger.info("Found existing session {} for phone number {}", session.sessionId(), userPhoneNumber))
|
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
|
||||||
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
|
||||||
logger.info("No existing session found for phone number {}. Creating new session: {}", userPhoneNumber, newSessionId);
|
|
||||||
return Mono.just(ConversationSessionDTO.create(newSessionId, userId, userPhoneNumber));
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
logger.warn("No phone number provided in request. Cannot manage conversation session without it.");
|
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 sessionMono.flatMap(session -> {
|
logger.info("Primary Check (MemoryStore): Looking up session for phone number: {}", userPhoneNumber);
|
||||||
final String finalSessionId = session.sessionId();
|
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||||
|
.flatMap(session -> {
|
||||||
logger.info("Managing conversation for resolved session: {}", finalSessionId);
|
Instant now = Instant.now();
|
||||||
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText);
|
if (Duration.between(session.lastModified(), now).toHours() < SESSION_RESET_THRESHOLD_HOURS) {
|
||||||
|
logger.info("Recent Session Found: Session {} is within the 24-hour threshold. Proceeding to Dialogflow.", session.sessionId());
|
||||||
final DetectIntentRequestDTO requestToDialogflow;
|
return processDialogflowRequest(session, request, userId, userMessageText, userPhoneNumber, false);
|
||||||
Instant currentInteractionTimestamp = userEntry.timestamp();
|
|
||||||
if (session.lastModified() != null &&
|
|
||||||
Duration.between(session.lastModified(), currentInteractionTimestamp).toHours() >= SESSION_RESET_THRESHOLD_HOURS) {
|
|
||||||
|
|
||||||
logger.info("Session {} (last modified: {}) is older than {} hours. Adding '{}' parameter to Dialogflow request.",
|
|
||||||
session.sessionId(), session.lastModified(), SESSION_RESET_THRESHOLD_HOURS, CURRENT_PAGE_PARAM);
|
|
||||||
|
|
||||||
requestToDialogflow = request.withParameter(CURRENT_PAGE_PARAM, true);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
requestToDialogflow = request;
|
logger.info("Old Session Found: Session {} is older than the threshold. Proceeding to full lookup.", session.sessionId());
|
||||||
|
return fullLookupAndProcess(session, request, userId, userMessageText, userPhoneNumber);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
logger.info("No session found in MemoryStore. Performing full lookup to Firestore.");
|
||||||
|
return fullLookupAndProcess(null, request, userId, userMessageText, userPhoneNumber);
|
||||||
|
}))
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("Overall error handling conversation in ConversationManagerService: {}", e.getMessage(), e);
|
||||||
|
return Mono.error(new RuntimeException("Failed to process conversation due to an internal error.", e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<DetectIntentResponseDTO> fullLookupAndProcess(ConversationSessionDTO oldSession, DetectIntentRequestDTO request, String userId, String userMessageText, String userPhoneNumber) {
|
||||||
|
return firestoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||||
|
.map(conversationContextMapper::toTextWithLimits)
|
||||||
|
.defaultIfEmpty("")
|
||||||
|
.flatMap(conversationHistory -> {
|
||||||
|
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
||||||
|
logger.info("Creating new session {} after full lookup.", newSessionId);
|
||||||
|
ConversationSessionDTO newSession = ConversationSessionDTO.create(newSessionId, userId, userPhoneNumber);
|
||||||
|
DetectIntentRequestDTO newRequest = request.withParameter(CURRENT_PAGE_PARAM, conversationHistory);
|
||||||
|
return processDialogflowRequest(newSession, newRequest, userId, userMessageText, userPhoneNumber, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 this.persistConversationTurn(userId, finalSessionId, userEntry, userPhoneNumber)
|
return this.persistConversationTurn(userId, finalSessionId, userEntry, userPhoneNumber)
|
||||||
.doOnSuccess(v -> logger.debug("User entry successfully persisted for session {}. Proceeding to Dialogflow...", finalSessionId))
|
.doOnSuccess(v -> logger.debug("User entry successfully persisted for session {}. Proceeding to Dialogflow...", finalSessionId))
|
||||||
.doOnError(e -> logger.error("Error during user entry persistence for session {}: {}", finalSessionId, e.getMessage(), e))
|
.doOnError(e -> logger.error("Error during user entry persistence for session {}: {}", finalSessionId, e.getMessage(), e))
|
||||||
.then(Mono.defer(() -> {
|
.then(Mono.defer(() -> dialogflowServiceClient.detectIntent(finalSessionId, request)
|
||||||
return dialogflowServiceClient.detectIntent(finalSessionId, requestToDialogflow)
|
.flatMap(response -> {
|
||||||
.doOnSuccess(response -> {
|
|
||||||
logger.debug("Received Dialogflow CX response for session {}. Initiating agent response persistence.", finalSessionId);
|
logger.debug("Received Dialogflow CX response for session {}. Initiating agent response persistence.", finalSessionId);
|
||||||
ConversationEntryDTO agentEntry = ConversationEntryDTO.forAgent(response.queryResult());
|
ConversationEntryDTO agentEntry = ConversationEntryDTO.forAgent(response.queryResult());
|
||||||
this.persistConversationTurn(userId, finalSessionId, agentEntry, userPhoneNumber).subscribe(
|
return persistConversationTurn(userId, finalSessionId, agentEntry, userPhoneNumber)
|
||||||
v -> logger.debug("Background: Agent entry persistence initiated for session {}.", finalSessionId),
|
.thenReturn(response);
|
||||||
e -> logger.error("Background: Error during agent entry persistence for session {}: {}", finalSessionId, e.getMessage(), e)
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.doOnError(error -> logger.error("Overall error during conversation management for session {}: {}", finalSessionId, error.getMessage(), error));
|
.doOnError(error -> logger.error("Overall error during conversation management for session {}: {}", finalSessionId, error.getMessage(), error))
|
||||||
}));
|
));
|
||||||
})
|
|
||||||
.onErrorResume(e -> {
|
|
||||||
logger.error("Overall error handling conversation in ConversationManagerService: {}", e.getMessage(), e);
|
|
||||||
return Mono.error(new RuntimeException("Failed to process conversation due to an internal error.", e));
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> persistConversationTurn(String userId, String sessionId, ConversationEntryDTO entry, String userPhoneNumber) {
|
private Mono<DetectIntentResponseDTO> startNotificationConversation(ConversationContext context, DetectIntentRequestDTO request, NotificationDTO notification) {
|
||||||
logger.debug("Starting Write-Back persistence for session {}. Type: {}. Writing to Redis first.", sessionId, entry.type().name());
|
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();
|
||||||
|
logger.info("No existing notification session found for phone number {}. Creating new session: {}",
|
||||||
|
userPhoneNumber, newSessionId);
|
||||||
|
return Mono.just(ConversationSessionDTO.create(newSessionId, userId, userPhoneNumber));
|
||||||
|
}))
|
||||||
|
.flatMap(session -> {
|
||||||
|
final String sessionId = session.sessionId();
|
||||||
|
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText);
|
||||||
|
return memoryStoreNotificationService.saveEntry(userId, sessionId, userEntry, userPhoneNumber)
|
||||||
|
.then(dialogflowServiceClient.detectIntent(sessionId, request)
|
||||||
|
.doOnSuccess(response -> {
|
||||||
|
ConversationEntryDTO agentEntry = ConversationEntryDTO.forAgent(response.queryResult());
|
||||||
|
memoryStoreNotificationService.saveEntry(userId, sessionId, agentEntry, userPhoneNumber).subscribe();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
return memoryStoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
||||||
.doOnSuccess(v -> {
|
.doOnSuccess(v -> logger.info(
|
||||||
logger.info("Entry saved to Redis for session {}. Type: {}. Kicking off async Firestore write-back.", sessionId, entry.type().name());
|
"Entry saved to Redis for session {}. Type: {}. Kicking off async Firestore write-back.",
|
||||||
firestoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
sessionId, entry.type().name()))
|
||||||
.subscribe(
|
.then(firestoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
||||||
fsVoid -> logger.debug("Asynchronously (Write-Back): Entry successfully saved to Firestore for session {}. Type: {}.",
|
.doOnSuccess(fsVoid -> logger.debug(
|
||||||
sessionId, entry.type().name()),
|
"Asynchronously (Write-Back): Entry successfully saved to Firestore for session {}. Type: {}.",
|
||||||
fsError -> logger.error("Asynchronously (Write-Back): Failed to save entry to Firestore for session {}. Type: {}: {}",
|
sessionId, entry.type().name()))
|
||||||
sessionId, entry.type().name(), fsError.getMessage(), fsError)
|
.doOnError(fsError -> logger.error(
|
||||||
);
|
"Asynchronously (Write-Back): Failed to save entry to Firestore for session {}. Type: {}: {}",
|
||||||
})
|
sessionId, entry.type().name(), fsError.getMessage(), fsError)))
|
||||||
.doOnError(e -> logger.error("Error during primary Redis write for session {}. Type: {}: {}", sessionId, entry.type().name(), e.getMessage(), e));
|
.doOnError(e -> logger.error("Error during primary Redis write for session {}. Type: {}: {}", sessionId,
|
||||||
|
entry.type().name(), e.getMessage(), e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
|
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
|
||||||
Map<String, Object> params = Optional.ofNullable(request.queryParams())
|
Map<String, Object> params = Optional.ofNullable(request.queryParams())
|
||||||
.map(queryParamsDTO -> queryParamsDTO.parameters())
|
.map(queryParamsDTO -> queryParamsDTO.parameters())
|
||||||
.orElse(Collections.emptyMap());
|
.orElse(Collections.emptyMap());
|
||||||
@@ -166,11 +259,13 @@ private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO req
|
|||||||
if (telefonoObj instanceof String) {
|
if (telefonoObj instanceof String) {
|
||||||
primaryPhoneNumber = (String) telefonoObj;
|
primaryPhoneNumber = (String) telefonoObj;
|
||||||
} else if (telefonoObj != null) {
|
} else if (telefonoObj != null) {
|
||||||
logger.warn("Parameter 'telefono' in queryParams is not a String (type: {}). Expected String.", telefonoObj.getClass().getName());
|
logger.warn("Parameter 'telefono' in queryParams is not a String (type: {}). Expected String.",
|
||||||
|
telefonoObj.getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primaryPhoneNumber == null || primaryPhoneNumber.trim().isEmpty()) {
|
if (primaryPhoneNumber == null || primaryPhoneNumber.trim().isEmpty()) {
|
||||||
throw new IllegalArgumentException("Phone number (telefono) is required in query parameters for conversation management.");
|
throw new IllegalArgumentException(
|
||||||
|
"Phone number (telefono) is required in query parameters for conversation management.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String resolvedUserId = null;
|
String resolvedUserId = null;
|
||||||
@@ -178,12 +273,14 @@ private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO req
|
|||||||
if (userIdObj instanceof String) {
|
if (userIdObj instanceof String) {
|
||||||
resolvedUserId = (String) userIdObj;
|
resolvedUserId = (String) userIdObj;
|
||||||
} else if (userIdObj != null) {
|
} else if (userIdObj != null) {
|
||||||
logger.warn("Parameter 'userId' in queryParams is not a String (type: {}). Expected String.", userIdObj.getClass().getName());
|
logger.warn("Parameter 'userId' in query_params is not a String (type: {}). Expected String.",
|
||||||
|
userIdObj.getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedUserId == null || resolvedUserId.trim().isEmpty()) {
|
if (resolvedUserId == null || resolvedUserId.trim().isEmpty()) {
|
||||||
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", "");
|
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", "");
|
||||||
logger.warn("User ID not provided in query parameters. Using derived ID from phone number: {}", resolvedUserId);
|
logger.warn("User ID not provided in query parameters. Using derived ID from phone number: {}",
|
||||||
|
resolvedUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.queryInput() == null || request.queryInput().text() == null ||
|
if (request.queryInput() == null || request.queryInput().text() == null ||
|
||||||
@@ -193,5 +290,5 @@ private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO req
|
|||||||
|
|
||||||
String userMessageText = request.queryInput().text().text();
|
String userMessageText = request.queryInput().text().text();
|
||||||
return new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
return new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,10 @@ public class FirestoreConversationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
||||||
|
return saveEntry(userId, sessionId, newEntry, userPhoneNumber, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber, String pantallaContexto) {
|
||||||
logger.info("Attempting to save conversation entry to Firestore for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
logger.info("Attempting to save conversation entry to Firestore for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
||||||
return Mono.fromRunnable(() -> {
|
return Mono.fromRunnable(() -> {
|
||||||
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
||||||
@@ -52,12 +56,15 @@ public class FirestoreConversationService {
|
|||||||
if (firestoreBaseRepository.documentExists(sessionDocRef)) {
|
if (firestoreBaseRepository.documentExists(sessionDocRef)) {
|
||||||
// Update: Append the new entry using arrayUnion and update lastModified
|
// Update: Append the new entry using arrayUnion and update lastModified
|
||||||
Map<String, Object> updates = firestoreConversationMapper.createUpdateMapForSingleEntry(newEntry);
|
Map<String, Object> updates = firestoreConversationMapper.createUpdateMapForSingleEntry(newEntry);
|
||||||
|
if (pantallaContexto != null) {
|
||||||
|
updates.put("pantallaContexto", pantallaContexto);
|
||||||
|
}
|
||||||
batch.update(sessionDocRef, updates);
|
batch.update(sessionDocRef, updates);
|
||||||
logger.info("Appending entry to existing conversation session for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
|
logger.info("Appending entry to existing conversation session for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
|
||||||
} else {
|
} else {
|
||||||
// Create: Start a new session with the first entry.
|
// Create: Start a new session with the first entry.
|
||||||
// Pass userId and userPhoneNumber to the mapper to be stored as fields in the document.
|
// Pass userId and userPhoneNumber to the mapper to be stored as fields in the document.
|
||||||
Map<String, Object> newSessionMap = firestoreConversationMapper.createNewSessionMapForSingleEntry(sessionId, userId, userPhoneNumber, newEntry);
|
Map<String, Object> newSessionMap = firestoreConversationMapper.createNewSessionMapForSingleEntry(sessionId, userId, userPhoneNumber, newEntry, pantallaContexto);
|
||||||
batch.set(sessionDocRef, newSessionMap);
|
batch.set(sessionDocRef, newSessionMap);
|
||||||
logger.info("Creating new conversation session with first entry for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
|
logger.info("Creating new conversation session with first entry for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
|
||||||
}
|
}
|
||||||
@@ -94,6 +101,20 @@ public class FirestoreConversationService {
|
|||||||
}).subscribeOn(Schedulers.boundedElastic());
|
}).subscribeOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Mono<ConversationSessionDTO> getSessionByTelefono(String userPhoneNumber) {
|
||||||
|
logger.info("Attempting to retrieve conversation session for phone number {}.", userPhoneNumber);
|
||||||
|
return firestoreBaseRepository.getDocumentsByField(getConversationCollectionPath(), "userPhoneNumber", userPhoneNumber)
|
||||||
|
.map(documentSnapshot -> {
|
||||||
|
if (documentSnapshot != null && documentSnapshot.exists()) {
|
||||||
|
ConversationSessionDTO sessionDTO = firestoreConversationMapper.mapFirestoreDocumentToConversationSessionDTO(documentSnapshot);
|
||||||
|
logger.info("Successfully retrieved and mapped conversation session for session {}.", sessionDTO.sessionId());
|
||||||
|
return sessionDTO;
|
||||||
|
}
|
||||||
|
logger.info("Conversation session not found for phone number {}.", userPhoneNumber);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private String getConversationCollectionPath() {
|
private String getConversationCollectionPath() {
|
||||||
return String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
return String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,28 +29,48 @@ public class MemoryStoreConversationService {
|
|||||||
private static final Duration SESSION_TTL = Duration.ofHours(24);
|
private static final Duration SESSION_TTL = Duration.ofHours(24);
|
||||||
private final ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate;
|
private final ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate;
|
||||||
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public MemoryStoreConversationService(
|
public MemoryStoreConversationService(
|
||||||
ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate,
|
ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate,
|
||||||
ReactiveRedisTemplate<String, String> stringRedisTemplate) {
|
ReactiveRedisTemplate<String, String> stringRedisTemplate,
|
||||||
|
FirestoreConversationService firestoreConversationService) {
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
this.stringRedisTemplate = stringRedisTemplate;
|
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) {
|
||||||
|
return saveEntry(userId, sessionId, newEntry, userPhoneNumber, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber, String pantallaContexto) {
|
||||||
String sessionKey = SESSION_KEY_PREFIX + sessionId;
|
String sessionKey = SESSION_KEY_PREFIX + sessionId;
|
||||||
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + userPhoneNumber;
|
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + userPhoneNumber;
|
||||||
|
|
||||||
|
|
||||||
logger.info("Attempting to save entry to Redis for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
logger.info("Attempting to save entry to Redis for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
||||||
|
|
||||||
|
|
||||||
return redisTemplate.opsForValue().get(sessionKey)
|
return redisTemplate.opsForValue().get(sessionKey)
|
||||||
.defaultIfEmpty(ConversationSessionDTO.create(sessionId, userId, userPhoneNumber))
|
.defaultIfEmpty(ConversationSessionDTO.create(sessionId, userId, userPhoneNumber))
|
||||||
.flatMap(session -> {
|
.flatMap(session -> {
|
||||||
ConversationSessionDTO sessionWithUpdatedTelefono = session.withTelefono(userPhoneNumber);
|
ConversationSessionDTO sessionWithUpdatedTelefono = session.withTelefono(userPhoneNumber);
|
||||||
ConversationSessionDTO updatedSession = sessionWithUpdatedTelefono.withAddedEntry(newEntry);
|
ConversationSessionDTO sessionWithPantallaContexto = (pantallaContexto != null) ? sessionWithUpdatedTelefono.withPantallaContexto(pantallaContexto) : sessionWithUpdatedTelefono;
|
||||||
|
ConversationSessionDTO updatedSession = sessionWithPantallaContexto.withAddedEntry(newEntry);
|
||||||
|
|
||||||
|
|
||||||
logger.info("Attempting to set updated session {} with new entry entity {} in Redis.", sessionId, newEntry.entity().name());
|
logger.info("Attempting to set updated session {} with new entry entity {} in Redis.", sessionId, newEntry.entity().name());
|
||||||
|
|
||||||
|
|
||||||
return redisTemplate.opsForValue().set(sessionKey, updatedSession, SESSION_TTL)
|
return redisTemplate.opsForValue().set(sessionKey, updatedSession, SESSION_TTL)
|
||||||
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, SESSION_TTL))
|
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, SESSION_TTL))
|
||||||
.then(); // <--- ADD THIS .then() WITHOUT ARGUMENTS
|
.then();
|
||||||
|
})
|
||||||
|
.doOnSuccess(success -> {
|
||||||
|
logger.info("Successfully saved updated session and phone mapping to Redis for session {}. Entity Type: {}", sessionId, newEntry.entity().name());
|
||||||
})
|
})
|
||||||
.doOnSuccess(success -> logger.info("Successfully saved updated session and phone mapping to Redis for session {}. Entity Type: {}", sessionId, newEntry.entity().name()))
|
|
||||||
.doOnError(e -> logger.error("Error appending entry to Redis for session {}: {}", sessionId, e.getMessage(), e));
|
.doOnError(e -> logger.error("Error appending entry to Redis for session {}: {}", sessionId, e.getMessage(), e));
|
||||||
}
|
}
|
||||||
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
|
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
|
import com.example.exception.FirestorePersistenceException;
|
||||||
|
import com.example.mapper.conversation.FirestoreConversationMapper;
|
||||||
|
import com.example.repository.FirestoreBaseRepository;
|
||||||
|
import com.google.cloud.firestore.DocumentReference;
|
||||||
|
import com.google.cloud.firestore.DocumentSnapshot;
|
||||||
|
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 for managing notification conversation sessions in Firestore.
|
||||||
|
* It handles the persistence of conversation entries, either by creating
|
||||||
|
* a new document for a new session or appending an entry to an existing
|
||||||
|
* session document using a Firestore batch. The service also provides
|
||||||
|
* methods for retrieving a complete conversation session from Firestore.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class FirestoreNotificationConvService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FirestoreNotificationConvService.class);
|
||||||
|
private static final String CONVERSATION_COLLECTION_PATH_FORMAT = "artifacts/%s/conversation-notifications";
|
||||||
|
private final FirestoreBaseRepository firestoreBaseRepository;
|
||||||
|
private final FirestoreConversationMapper firestoreConversationMapper;
|
||||||
|
|
||||||
|
public FirestoreNotificationConvService(FirestoreBaseRepository firestoreBaseRepository, FirestoreConversationMapper firestoreConversationMapper) {
|
||||||
|
this.firestoreBaseRepository = firestoreBaseRepository;
|
||||||
|
this.firestoreConversationMapper = firestoreConversationMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
||||||
|
logger.info("Attempting to save conversation entry to Firestore for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
||||||
|
// Synchronize on the session ID to prevent race conditions when creating a new session.
|
||||||
|
synchronized (sessionId.intern()) {
|
||||||
|
WriteBatch batch = firestoreBaseRepository.createBatch();
|
||||||
|
try {
|
||||||
|
if (firestoreBaseRepository.documentExists(sessionDocRef)) {
|
||||||
|
// Update: Append the new entry using arrayUnion and update lastModified
|
||||||
|
Map<String, Object> updates = firestoreConversationMapper.createUpdateMapForSingleEntry(newEntry);
|
||||||
|
batch.update(sessionDocRef, updates);
|
||||||
|
logger.info("Appending entry to existing conversation session for user {} and session {}. Entity: {}", userId, sessionId, newEntry.entity().name());
|
||||||
|
} else {
|
||||||
|
// Create: Start a new session with the first entry.
|
||||||
|
// Pass userId and userPhoneNumber to the mapper to be stored as fields in the document.
|
||||||
|
Map<String, Object> newSessionMap = firestoreConversationMapper.createNewSessionMapForSingleEntry(sessionId, userId, userPhoneNumber, newEntry);
|
||||||
|
batch.set(sessionDocRef, newSessionMap);
|
||||||
|
logger.info("Creating new conversation 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 conversation entry to Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||||
|
throw new FirestorePersistenceException("Failed to save conversation entry to Firestore for session " + sessionId, e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.error("Thread interrupted while saving conversation entry to Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||||
|
throw new FirestorePersistenceException("Saving conversation entry was interrupted for session " + sessionId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).subscribeOn(Schedulers.boundedElastic()).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<ConversationSessionDTO> getConversationSession(String userId, String sessionId) {
|
||||||
|
logger.info("Attempting to retrieve conversation session for session {} (user ID {} for context).", sessionId, userId);
|
||||||
|
return Mono.fromCallable(() -> {
|
||||||
|
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
||||||
|
try {
|
||||||
|
DocumentSnapshot documentSnapshot = firestoreBaseRepository.getDocumentSnapshot(sessionDocRef);
|
||||||
|
if (documentSnapshot != null && documentSnapshot.exists()) {
|
||||||
|
ConversationSessionDTO sessionDTO = firestoreConversationMapper.mapFirestoreDocumentToConversationSessionDTO(documentSnapshot);
|
||||||
|
logger.info("Successfully retrieved and mapped conversation session for session {}.", sessionId);
|
||||||
|
return sessionDTO;
|
||||||
|
}
|
||||||
|
logger.info("Conversation session not found for session {}.", sessionId);
|
||||||
|
return null; // Or Mono.empty() if this method returned Mono<Optional<ConversationSessionDTO>>
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
logger.error("Error retrieving conversation session from Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||||
|
throw new FirestorePersistenceException("Failed to retrieve conversation session from Firestore for session " + sessionId, e);
|
||||||
|
}
|
||||||
|
}).subscribeOn(Schedulers.boundedElastic());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
package com.example.service.notification;
|
||||||
|
|
||||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||||
@@ -7,15 +12,18 @@ import com.example.repository.FirestoreBaseRepository;
|
|||||||
import com.google.cloud.Timestamp;
|
import com.google.cloud.Timestamp;
|
||||||
import com.google.cloud.firestore.DocumentReference;
|
import com.google.cloud.firestore.DocumentReference;
|
||||||
import com.google.cloud.firestore.FieldValue;
|
import com.google.cloud.firestore.FieldValue;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class FirestoreNotificationService {
|
public class FirestoreNotificationService {
|
||||||
@@ -32,58 +40,84 @@ public class FirestoreNotificationService {
|
|||||||
|
|
||||||
public FirestoreNotificationService(
|
public FirestoreNotificationService(
|
||||||
FirestoreBaseRepository firestoreBaseRepository,
|
FirestoreBaseRepository firestoreBaseRepository,
|
||||||
FirestoreNotificationMapper firestoreNotificationMapper) {
|
FirestoreNotificationMapper firestoreNotificationMapper,
|
||||||
|
MemoryStoreNotificationService memoryStoreNotificationService) {
|
||||||
this.firestoreBaseRepository = firestoreBaseRepository;
|
this.firestoreBaseRepository = firestoreBaseRepository;
|
||||||
this.firestoreNotificationMapper = firestoreNotificationMapper;
|
this.firestoreNotificationMapper = firestoreNotificationMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Mono<Void> saveOrAppendNotificationEntry(NotificationDTO newEntry) {
|
public Mono<Void> saveOrAppendNotificationEntry(NotificationDTO newEntry) {
|
||||||
return Mono.fromRunnable(() -> {
|
return Mono.fromRunnable(
|
||||||
|
() -> {
|
||||||
String phoneNumber = newEntry.telefono();
|
String phoneNumber = newEntry.telefono();
|
||||||
if (phoneNumber == null || phoneNumber.isBlank()) {
|
if (phoneNumber == null || phoneNumber.isBlank()) {
|
||||||
throw new IllegalArgumentException("Phone number is required to manage notification entries.");
|
throw new IllegalArgumentException(
|
||||||
|
"Phone number is required to manage notification entries.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the phone number as the document ID for the session.
|
// Use the phone number as the document ID for the session.
|
||||||
String notificationSessionId = phoneNumber;
|
String notificationSessionId = phoneNumber;
|
||||||
|
|
||||||
DocumentReference notificationDocRef = getNotificationDocumentReference(notificationSessionId);
|
// Synchronize on the notification session ID to prevent race conditions when creating a new session.
|
||||||
Map<String, Object> entryMap = firestoreNotificationMapper.mapNotificationDTOToMap(newEntry);
|
synchronized (notificationSessionId.intern()) {
|
||||||
|
DocumentReference notificationDocRef =
|
||||||
|
getNotificationDocumentReference(notificationSessionId);
|
||||||
|
Map<String, Object> entryMap =
|
||||||
|
firestoreNotificationMapper.mapNotificationDTOToMap(newEntry);
|
||||||
try {
|
try {
|
||||||
// Check if the session document exists.
|
// Check if the session document exists.
|
||||||
boolean docExists = notificationDocRef.get().get().exists();
|
boolean docExists = firestoreBaseRepository.documentExists(notificationDocRef);
|
||||||
|
|
||||||
if (docExists) {
|
if (docExists) {
|
||||||
// If the document exists, append the new entry to the 'notificaciones' array.
|
// If the document exists, append the new entry to the 'notificaciones' array.
|
||||||
Map<String, Object> updates = Map.of(
|
Map<String, Object> updates =
|
||||||
|
Map.of(
|
||||||
FIELD_MESSAGES, FieldValue.arrayUnion(entryMap),
|
FIELD_MESSAGES, FieldValue.arrayUnion(entryMap),
|
||||||
FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now()))
|
FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())));
|
||||||
);
|
firestoreBaseRepository.updateDocument(notificationDocRef, updates);
|
||||||
notificationDocRef.update(updates).get();
|
logger.info(
|
||||||
logger.info("Successfully appended new entry to notification session {} in Firestore.", notificationSessionId);
|
"Successfully appended new entry to notification session {} in Firestore.",
|
||||||
|
notificationSessionId);
|
||||||
} else {
|
} else {
|
||||||
// If the document does not exist, create a new session document.
|
// If the document does not exist, create a new session document.
|
||||||
Map<String, Object> newSessionData = Map.of(
|
Map<String, Object> newSessionData =
|
||||||
FIELD_NOTIFICATION_ID, notificationSessionId,
|
Map.of(
|
||||||
FIELD_PHONE_NUMBER, phoneNumber,
|
FIELD_NOTIFICATION_ID,
|
||||||
"fechaCreacion", Timestamp.of(java.util.Date.from(Instant.now())),
|
notificationSessionId,
|
||||||
FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())),
|
FIELD_PHONE_NUMBER,
|
||||||
FIELD_MESSAGES, Collections.singletonList(entryMap)
|
phoneNumber,
|
||||||
);
|
"fechaCreacion",
|
||||||
notificationDocRef.set(newSessionData).get();
|
Timestamp.of(java.util.Date.from(Instant.now())),
|
||||||
logger.info("Successfully created a new notification session {} in Firestore.", notificationSessionId);
|
FIELD_LAST_UPDATED,
|
||||||
|
Timestamp.of(java.util.Date.from(Instant.now())),
|
||||||
|
FIELD_MESSAGES,
|
||||||
|
Collections.singletonList(entryMap));
|
||||||
|
firestoreBaseRepository.setDocument(notificationDocRef, newSessionData);
|
||||||
|
logger.info(
|
||||||
|
"Successfully created a new notification session {} in Firestore.",
|
||||||
|
notificationSessionId);
|
||||||
}
|
}
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
logger.error("Error saving notification to Firestore for phone {}: {}", phoneNumber, e.getMessage(), e);
|
logger.error(
|
||||||
throw new FirestorePersistenceException("Failed to save notification to Firestore for phone " + phoneNumber, e);
|
"Error saving notification to Firestore for phone {}: {}",
|
||||||
|
phoneNumber,
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
throw new FirestorePersistenceException(
|
||||||
|
"Failed to save notification to Firestore for phone " + phoneNumber, e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
logger.error("Thread interrupted while saving notification to Firestore for phone {}: {}", phoneNumber, e.getMessage(), e);
|
logger.error(
|
||||||
throw new FirestorePersistenceException("Saving notification was interrupted for phone " + phoneNumber, e);
|
"Thread interrupted while saving notification to Firestore for phone {}: {}",
|
||||||
|
phoneNumber,
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
throw new FirestorePersistenceException(
|
||||||
|
"Saving notification was interrupted for phone " + phoneNumber, e);
|
||||||
}
|
}
|
||||||
}).subscribeOn(Schedulers.boundedElastic()).then();
|
}
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getNotificationCollectionPath() {
|
private String getNotificationCollectionPath() {
|
||||||
@@ -94,4 +128,41 @@ public class FirestoreNotificationService {
|
|||||||
String collectionPath = getNotificationCollectionPath();
|
String collectionPath = getNotificationCollectionPath();
|
||||||
return firestoreBaseRepository.getDocumentReference(collectionPath, notificationId);
|
return firestoreBaseRepository.getDocumentReference(collectionPath, notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Mono<Void> updateNotificationStatus(String sessionId, String status) {
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
DocumentReference notificationDocRef = getNotificationDocumentReference(sessionId);
|
||||||
|
try {
|
||||||
|
Map<String, Object> sessionData = firestoreBaseRepository.getDocument(notificationDocRef, Map.class);
|
||||||
|
if (sessionData != null) {
|
||||||
|
List<Map<String, Object>> notifications = (List<Map<String, Object>>) sessionData.get(FIELD_MESSAGES);
|
||||||
|
if (notifications != null) {
|
||||||
|
List<Map<String, Object>> updatedNotifications = new ArrayList<>();
|
||||||
|
for (Map<String, Object> notification : notifications) {
|
||||||
|
Map<String, Object> updatedNotification = new HashMap<>(notification);
|
||||||
|
updatedNotification.put("status", status);
|
||||||
|
updatedNotifications.add(updatedNotification);
|
||||||
|
}
|
||||||
|
Map<String, Object> updates = new HashMap<>();
|
||||||
|
updates.put(FIELD_MESSAGES, updatedNotifications);
|
||||||
|
updates.put(FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())));
|
||||||
|
firestoreBaseRepository.updateDocument(notificationDocRef, updates);
|
||||||
|
logger.info("Successfully updated notification status to '{}' for session {} in Firestore.", status, sessionId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("Notification session {} not found in Firestore. Cannot update status.", sessionId);
|
||||||
|
}
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
logger.error("Error updating notification status in Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||||
|
throw new FirestorePersistenceException("Failed to update notification status in Firestore for session " + sessionId, e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.error("Thread interrupted while updating notification status in Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||||
|
throw new FirestorePersistenceException("Updating notification status was interrupted for session " + sessionId, e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
.then();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.example.service.notification;
|
package com.example.service.notification;
|
||||||
|
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||||
import com.example.dto.dialogflow.notification.NotificationSessionDTO;
|
import com.example.dto.dialogflow.notification.NotificationSessionDTO;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -19,17 +21,25 @@ public class MemoryStoreNotificationService {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MemoryStoreNotificationService.class);
|
private static final Logger logger = LoggerFactory.getLogger(MemoryStoreNotificationService.class);
|
||||||
private final ReactiveRedisTemplate<String, NotificationSessionDTO> notificationRedisTemplate;
|
private final ReactiveRedisTemplate<String, NotificationSessionDTO> notificationRedisTemplate;
|
||||||
|
private final ReactiveRedisTemplate<String, ConversationSessionDTO> conversationRedisTemplate;
|
||||||
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
||||||
|
private final FirestoreNotificationConvService firestoreNotificationConvService;
|
||||||
private static final String NOTIFICATION_KEY_PREFIX = "notification:";
|
private static final String NOTIFICATION_KEY_PREFIX = "notification:";
|
||||||
private static final String PHONE_TO_SESSION_KEY_PREFIX = "notification:phone_to_notification:";
|
private static final String PHONE_TO_NOTIFICATION_SESSION_KEY_PREFIX = "notification:phone_to_notification:";
|
||||||
private final Duration notificationTtl = Duration.ofHours(24);
|
private static final String CONVERSATION_SESSION_KEY_PREFIX = "conversation-notification:session:";
|
||||||
|
private static final String PHONE_TO_CONVERSATION_SESSION_KEY_PREFIX = "conversation-notification:phone_to_session:";
|
||||||
|
private final Duration notificationTtl = Duration.ofMinutes(5);
|
||||||
|
|
||||||
public MemoryStoreNotificationService(
|
public MemoryStoreNotificationService(
|
||||||
ReactiveRedisTemplate<String, NotificationSessionDTO> notificationRedisTemplate,
|
ReactiveRedisTemplate<String, NotificationSessionDTO> notificationRedisTemplate,
|
||||||
|
ReactiveRedisTemplate<String, ConversationSessionDTO> conversationRedisTemplate,
|
||||||
ReactiveRedisTemplate<String, String> stringRedisTemplate,
|
ReactiveRedisTemplate<String, String> stringRedisTemplate,
|
||||||
|
FirestoreNotificationConvService firestoreNotificationConvService,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
this.notificationRedisTemplate = notificationRedisTemplate;
|
this.notificationRedisTemplate = notificationRedisTemplate;
|
||||||
|
this.conversationRedisTemplate = conversationRedisTemplate;
|
||||||
this.stringRedisTemplate = stringRedisTemplate;
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
this.firestoreNotificationConvService = firestoreNotificationConvService;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +48,6 @@ public class MemoryStoreNotificationService {
|
|||||||
if (phoneNumber == null || phoneNumber.isBlank()) {
|
if (phoneNumber == null || phoneNumber.isBlank()) {
|
||||||
return Mono.error(new IllegalArgumentException("Phone number is required to manage notification entries."));
|
return Mono.error(new IllegalArgumentException("Phone number is required to manage notification entries."));
|
||||||
}
|
}
|
||||||
|
|
||||||
//noote: Use the phone number as the session ID for notifications
|
//noote: Use the phone number as the session ID for notifications
|
||||||
String notificationSessionId = phoneNumber;
|
String notificationSessionId = phoneNumber;
|
||||||
|
|
||||||
@@ -73,7 +82,7 @@ public class MemoryStoreNotificationService {
|
|||||||
|
|
||||||
private Mono<Boolean> cacheNotificationSession(NotificationSessionDTO session) {
|
private Mono<Boolean> cacheNotificationSession(NotificationSessionDTO session) {
|
||||||
String key = NOTIFICATION_KEY_PREFIX + session.sessionId();
|
String key = NOTIFICATION_KEY_PREFIX + session.sessionId();
|
||||||
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + session.telefono();
|
String phoneToSessionKey = PHONE_TO_NOTIFICATION_SESSION_KEY_PREFIX + session.telefono();
|
||||||
|
|
||||||
return notificationRedisTemplate.opsForValue().set(key, session, notificationTtl)
|
return notificationRedisTemplate.opsForValue().set(key, session, notificationTtl)
|
||||||
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, session.sessionId(), notificationTtl));
|
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, session.sessionId(), notificationTtl));
|
||||||
@@ -93,7 +102,7 @@ public class MemoryStoreNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Mono<String> getNotificationIdForPhone(String phone) {
|
public Mono<String> getNotificationIdForPhone(String phone) {
|
||||||
String key = PHONE_TO_SESSION_KEY_PREFIX + phone;
|
String key = PHONE_TO_NOTIFICATION_SESSION_KEY_PREFIX + phone;
|
||||||
return stringRedisTemplate.opsForValue().get(key)
|
return stringRedisTemplate.opsForValue().get(key)
|
||||||
.doOnSuccess(sessionId -> {
|
.doOnSuccess(sessionId -> {
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
@@ -105,4 +114,53 @@ public class MemoryStoreNotificationService {
|
|||||||
.doOnError(e -> logger.error("Error retrieving session ID for phone {} from MemoryStore: {}", phone,
|
.doOnError(e -> logger.error("Error retrieving session ID for phone {} from MemoryStore: {}", phone,
|
||||||
e.getMessage(), e));
|
e.getMessage(), e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
||||||
|
String sessionKey = CONVERSATION_SESSION_KEY_PREFIX + sessionId;
|
||||||
|
String phoneToSessionKey = PHONE_TO_CONVERSATION_SESSION_KEY_PREFIX + userPhoneNumber;
|
||||||
|
logger.info("Attempting to save entry to Redis for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
||||||
|
return conversationRedisTemplate.opsForValue().get(sessionKey)
|
||||||
|
.defaultIfEmpty(ConversationSessionDTO.create(sessionId, userId, userPhoneNumber))
|
||||||
|
.flatMap(session -> {
|
||||||
|
ConversationSessionDTO sessionWithUpdatedTelefono = session.withTelefono(userPhoneNumber);
|
||||||
|
ConversationSessionDTO updatedSession = sessionWithUpdatedTelefono.withAddedEntry(newEntry);
|
||||||
|
logger.info("Attempting to set updated session {} with new entry entity {} in Redis.", sessionId, newEntry.entity().name());
|
||||||
|
return conversationRedisTemplate.opsForValue().set(sessionKey, updatedSession, notificationTtl)
|
||||||
|
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, notificationTtl))
|
||||||
|
.then();
|
||||||
|
})
|
||||||
|
.doOnSuccess(success ->{
|
||||||
|
logger.info("Successfully saved updated session and phone mapping to Redis for session {}. Entity Type: {}", sessionId, newEntry.entity().name());
|
||||||
|
firestoreNotificationConvService.saveEntry(userId, sessionId, newEntry, userPhoneNumber)
|
||||||
|
.subscribe(
|
||||||
|
fsVoid -> logger.debug(
|
||||||
|
"Asynchronously (Write-Back): Entry successfully saved to Firestore for session {}. Type: {}.",
|
||||||
|
sessionId, newEntry.type().name()),
|
||||||
|
fsError -> logger.error(
|
||||||
|
"Asynchronously (Write-Back): Failed to save entry to Firestore for session {}. Type: {}: {}",
|
||||||
|
sessionId, newEntry.type().name(), fsError.getMessage(), fsError));
|
||||||
|
})
|
||||||
|
.doOnError(e -> logger.error("Error appending entry to Redis for session {}: {}", sessionId, e.getMessage(), e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
|
||||||
|
if (telefono == null || telefono.isBlank()) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
String phoneToSessionKey = PHONE_TO_CONVERSATION_SESSION_KEY_PREFIX + telefono;
|
||||||
|
logger.debug("Attempting to retrieve session ID for phone number {} from Redis.", telefono);
|
||||||
|
return stringRedisTemplate.opsForValue().get(phoneToSessionKey)
|
||||||
|
.flatMap(sessionId -> {
|
||||||
|
logger.debug("Found session ID {} for phone number {}. Retrieving session data.", sessionId, telefono);
|
||||||
|
return conversationRedisTemplate.opsForValue().get(CONVERSATION_SESSION_KEY_PREFIX + sessionId);
|
||||||
|
})
|
||||||
|
.doOnSuccess(session -> {
|
||||||
|
if (session != null) {
|
||||||
|
logger.info("Successfully retrieved session {} by phone number {}.", session.sessionId(), telefono);
|
||||||
|
} else {
|
||||||
|
logger.info("No session found in Redis for phone number {}.", telefono);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnError(e -> logger.error("Error retrieving session by phone number {}: {}", telefono, e.getMessage(), e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.data.redis.connection.Message;
|
||||||
|
import org.springframework.data.redis.connection.MessageListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NotificationExpirationListener implements MessageListener {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(NotificationExpirationListener.class);
|
||||||
|
private final FirestoreNotificationService firestoreNotificationService;
|
||||||
|
private static final String NOTIFICATION_KEY_PREFIX = "notification:";
|
||||||
|
|
||||||
|
public NotificationExpirationListener(FirestoreNotificationService firestoreNotificationService) {
|
||||||
|
this.firestoreNotificationService = firestoreNotificationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(Message message, byte[] pattern) {
|
||||||
|
String expiredKey = new String(message.getBody());
|
||||||
|
logger.info("Expired key: " + expiredKey);
|
||||||
|
if (expiredKey.startsWith(NOTIFICATION_KEY_PREFIX)) {
|
||||||
|
String sessionId = expiredKey.substring(NOTIFICATION_KEY_PREFIX.length());
|
||||||
|
firestoreNotificationService.updateNotificationStatus(sessionId, "inactive")
|
||||||
|
.doOnSuccess(v -> logger.info("Notification status updated to inactive for session: " + sessionId))
|
||||||
|
.doOnError(e -> logger.error("Error updating notification status for session: " + sessionId, e))
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,12 @@ package com.example.service.notification;
|
|||||||
import com.example.dto.dialogflow.notification.ExternalNotRequestDTO;
|
import com.example.dto.dialogflow.notification.ExternalNotRequestDTO;
|
||||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||||
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
import com.example.dto.dialogflow.conversation.QueryInputDTO;
|
import com.example.dto.dialogflow.conversation.QueryInputDTO;
|
||||||
import com.example.dto.dialogflow.conversation.QueryParamsDTO;
|
import com.example.dto.dialogflow.conversation.QueryParamsDTO;
|
||||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||||
import com.example.service.base.DialogflowClientService;
|
import com.example.service.base.DialogflowClientService;
|
||||||
import com.example.service.conversation.MemoryStoreConversationService;
|
|
||||||
import com.example.util.SessionIdGenerator;
|
import com.example.util.SessionIdGenerator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -26,31 +26,30 @@ import com.example.dto.dialogflow.conversation.TextInputDTO;
|
|||||||
@Service
|
@Service
|
||||||
public class NotificationManagerService {
|
public class NotificationManagerService {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(NotificationManagerService.class);
|
private static final Logger logger = LoggerFactory.getLogger(NotificationManagerService.class);
|
||||||
private static final String NOTIFICATION_TEXT_PARAM = "notificationText";
|
private static final String NOTIFICATION_TEXT_PARAM = "notificationText";
|
||||||
private static final String eventName = "notificacion";
|
private static final String eventName = "notificacion";
|
||||||
|
|
||||||
|
private final DialogflowClientService dialogflowClientService;
|
||||||
|
private final FirestoreNotificationService firestoreNotificationService;
|
||||||
|
private final MemoryStoreNotificationService memoryStoreNotificationService;
|
||||||
|
private final FirestoreNotificationConvService firestoreConversationService;
|
||||||
|
|
||||||
private final DialogflowClientService dialogflowClientService;
|
@Value("${dialogflow.default-language-code:es}")
|
||||||
private final FirestoreNotificationService firestoreNotificationService;
|
private String defaultLanguageCode;
|
||||||
private final MemoryStoreNotificationService memoryStoreNotificationService;
|
|
||||||
private final MemoryStoreConversationService memoryStoreConversationService;
|
|
||||||
|
|
||||||
@Value("${dialogflow.default-language-code:es}")
|
public NotificationManagerService(
|
||||||
private String defaultLanguageCode;
|
|
||||||
|
|
||||||
public NotificationManagerService(
|
|
||||||
DialogflowClientService dialogflowClientService,
|
DialogflowClientService dialogflowClientService,
|
||||||
FirestoreNotificationService firestoreNotificationService,
|
FirestoreNotificationService firestoreNotificationService,
|
||||||
MemoryStoreNotificationService memoryStoreNotificationService,
|
MemoryStoreNotificationService memoryStoreNotificationService,
|
||||||
MemoryStoreConversationService memoryStoreConversationService) {
|
FirestoreNotificationConvService firestoreConversationService) {
|
||||||
this.dialogflowClientService = dialogflowClientService;
|
this.dialogflowClientService = dialogflowClientService;
|
||||||
this.firestoreNotificationService = firestoreNotificationService;
|
this.firestoreNotificationService = firestoreNotificationService;
|
||||||
this.memoryStoreNotificationService = memoryStoreNotificationService;
|
this.memoryStoreNotificationService = memoryStoreNotificationService;
|
||||||
this.memoryStoreConversationService = memoryStoreConversationService;
|
this.firestoreConversationService = firestoreConversationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<DetectIntentResponseDTO> processNotification(ExternalNotRequestDTO externalRequest) {
|
public Mono<DetectIntentResponseDTO> processNotification(ExternalNotRequestDTO externalRequest) {
|
||||||
Objects.requireNonNull(externalRequest, "ExternalNotRequestDTO cannot be null.");
|
Objects.requireNonNull(externalRequest, "ExternalNotRequestDTO cannot be null.");
|
||||||
|
|
||||||
String telefono = externalRequest.phoneNumber();
|
String telefono = externalRequest.phoneNumber();
|
||||||
@@ -61,29 +60,40 @@ public class NotificationManagerService {
|
|||||||
|
|
||||||
// 1. Persist the incoming notification entry
|
// 1. Persist the incoming notification entry
|
||||||
String newNotificationId = SessionIdGenerator.generateStandardSessionId();
|
String newNotificationId = SessionIdGenerator.generateStandardSessionId();
|
||||||
NotificationDTO newNotificationEntry = new NotificationDTO(newNotificationId,telefono, Instant.now(),
|
NotificationDTO newNotificationEntry = new NotificationDTO(newNotificationId, telefono, Instant.now(),
|
||||||
externalRequest.text(),eventName, defaultLanguageCode,Collections.emptyMap()
|
externalRequest.text(), eventName, defaultLanguageCode, Collections.emptyMap(), "active");
|
||||||
);
|
|
||||||
Mono<Void> persistenceMono = memoryStoreNotificationService.saveOrAppendNotificationEntry(newNotificationEntry)
|
Mono<Void> persistenceMono = memoryStoreNotificationService.saveOrAppendNotificationEntry(newNotificationEntry)
|
||||||
.doOnSuccess(v -> {
|
.doOnSuccess(v -> {
|
||||||
logger.info("Notification for phone {} cached. Kicking off async Firestore write-back.", telefono);
|
logger.info("Notification for phone {} cached. Kicking off async Firestore write-back.", telefono);
|
||||||
firestoreNotificationService.saveOrAppendNotificationEntry(newNotificationEntry)
|
firestoreNotificationService.saveOrAppendNotificationEntry(newNotificationEntry)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
ignored -> logger.debug("Background: Notification entry persistence initiated for phone {} in Firestore.", telefono),
|
ignored -> logger.debug(
|
||||||
e -> logger.error("Background: Error during notification entry persistence for phone {} in Firestore: {}", telefono, e.getMessage(), e)
|
"Background: Notification entry persistence initiated for phone {} in Firestore.",telefono),
|
||||||
);
|
e -> logger.error(
|
||||||
|
"Background: Error during notification entry persistence for phone {} in Firestore: {}",
|
||||||
|
telefono, e.getMessage(), e));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Resolve or create a conversation session
|
// 2. Resolve or create a conversation session
|
||||||
Mono<ConversationSessionDTO> sessionMono = memoryStoreConversationService.getSessionByTelefono(telefono)
|
Mono<ConversationSessionDTO> sessionMono = memoryStoreNotificationService.getSessionByTelefono(telefono)
|
||||||
.doOnNext(session -> logger.info("Found existing conversation session {} for phone number {}", session.sessionId(), telefono))
|
.doOnNext(session -> logger.info("Found existing conversation session {} for phone number {}",
|
||||||
|
session.sessionId(), telefono))
|
||||||
|
.flatMap(session -> {
|
||||||
|
ConversationEntryDTO systemEntry = ConversationEntryDTO.forSystem(externalRequest.text());
|
||||||
|
return persistConversationTurn(session.userId(), session.sessionId(), systemEntry, telefono)
|
||||||
|
.thenReturn(session);
|
||||||
|
})
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
||||||
logger.info("No existing conversation session found for phone number {}. Creating new session: {}", telefono, newSessionId);
|
logger.info("No existing conversation session found for phone number {}. Creating new session: {}",telefono, newSessionId);
|
||||||
return Mono.just(ConversationSessionDTO.create(newSessionId, "user_by_phone_" + telefono, telefono));
|
String userId = "user_by_phone_" + telefono;
|
||||||
|
ConversationEntryDTO systemEntry = ConversationEntryDTO.forSystem(externalRequest.text());
|
||||||
|
return persistConversationTurn(userId, newSessionId, systemEntry, telefono)
|
||||||
|
.then(Mono.just(ConversationSessionDTO.create(newSessionId, userId, telefono)));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 3. Send notification text to Dialogflow using the resolved conversation session
|
// 3. Send notification text to Dialogflow using the resolved conversation
|
||||||
|
// session
|
||||||
return persistenceMono.then(sessionMono)
|
return persistenceMono.then(sessionMono)
|
||||||
.flatMap(session -> {
|
.flatMap(session -> {
|
||||||
final String sessionId = session.sessionId();
|
final String sessionId = session.sessionId();
|
||||||
@@ -99,12 +109,35 @@ public class NotificationManagerService {
|
|||||||
|
|
||||||
DetectIntentRequestDTO detectIntentRequest = new DetectIntentRequestDTO(
|
DetectIntentRequestDTO detectIntentRequest = new DetectIntentRequestDTO(
|
||||||
queryInput,
|
queryInput,
|
||||||
new QueryParamsDTO(parameters)
|
new QueryParamsDTO(parameters));
|
||||||
);
|
|
||||||
|
|
||||||
return dialogflowClientService.detectIntent(sessionId, detectIntentRequest);
|
return dialogflowClientService.detectIntent(sessionId, detectIntentRequest);
|
||||||
})
|
})
|
||||||
.doOnSuccess(response -> logger.info("Finished processing notification. Dialogflow response received for phone {}.", telefono))
|
.doOnSuccess(response -> logger
|
||||||
|
.info("Finished processing notification. Dialogflow response received for phone {}.", telefono))
|
||||||
.doOnError(e -> logger.error("Overall error in NotificationManagerService: {}", e.getMessage(), e));
|
.doOnError(e -> logger.error("Overall error in NotificationManagerService: {}", e.getMessage(), e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 memoryStoreNotificationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
||||||
|
.doOnSuccess(v -> {
|
||||||
|
logger.info(
|
||||||
|
"Entry saved to Redis for session {}. Type: {}. Kicking off async Firestore write-back.",
|
||||||
|
sessionId, entry.type().name());
|
||||||
|
firestoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
||||||
|
.subscribe(
|
||||||
|
fsVoid -> logger.debug(
|
||||||
|
"Asynchronously (Write-Back): Entry successfully saved to Firestore for session {}. Type: {}.",
|
||||||
|
sessionId, entry.type().name()),
|
||||||
|
fsError -> logger.error(
|
||||||
|
"Asynchronously (Write-Back): Failed to save entry to Firestore for session {}. Type: {}: {}",
|
||||||
|
sessionId, entry.type().name(), fsError.getMessage(), fsError));
|
||||||
|
})
|
||||||
|
.doOnError(e -> logger.error("Error during primary Redis write for session {}. Type: {}: {}", sessionId,
|
||||||
|
entry.type().name(), e.getMessage(), e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MemoryStoreQRService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MemoryStoreQRService.class);
|
||||||
|
private static final String SESSION_KEY_PREFIX = "qr:session:";
|
||||||
|
private static final String PHONE_TO_SESSION_KEY_PREFIX = "qr:phone_to_session:";
|
||||||
|
private static final Duration SESSION_TTL = Duration.ofHours(24);
|
||||||
|
private final ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate;
|
||||||
|
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MemoryStoreQRService(
|
||||||
|
ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate,
|
||||||
|
ReactiveRedisTemplate<String, String> stringRedisTemplate) {
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
return redisTemplate.opsForValue().get(sessionKey)
|
||||||
|
.defaultIfEmpty(ConversationSessionDTO.create(sessionId, userId, userPhoneNumber))
|
||||||
|
.flatMap(session -> {
|
||||||
|
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());
|
||||||
|
|
||||||
|
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());
|
||||||
|
})
|
||||||
|
.doOnError(e -> logger.error("Error appending entry to Redis for quick reply session {}: {}", sessionId, e.getMessage(), e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
|
||||||
|
if (telefono == null || telefono.isBlank()) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + telefono;
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
} 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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.base.DetectIntentResponseDTO;
|
||||||
|
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.ExternalConvRequestDTO;
|
||||||
|
import com.example.dto.quickreplies.QuickReplyScreenRequestDTO;
|
||||||
|
import com.example.service.conversation.FirestoreConversationService;
|
||||||
|
import com.example.service.conversation.MemoryStoreConversationService;
|
||||||
|
import com.example.util.SessionIdGenerator;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public QuickRepliesManagerService(
|
||||||
|
MemoryStoreConversationService memoryStoreConversationService,
|
||||||
|
FirestoreConversationService firestoreConversationService,
|
||||||
|
QuickReplyContentService quickReplyContentService) {
|
||||||
|
this.memoryStoreConversationService = memoryStoreConversationService;
|
||||||
|
this.firestoreConversationService = firestoreConversationService;
|
||||||
|
this.quickReplyContentService = quickReplyContentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||||
|
.flatMap(session -> Mono.just(session.sessionId()))
|
||||||
|
.switchIfEmpty(Mono.fromCallable(SessionIdGenerator::generateStandardSessionId))
|
||||||
|
.flatMap(sessionId -> {
|
||||||
|
String userId = "user_by_phone_" + userPhoneNumber.replaceAll("[^0-9]", "");
|
||||||
|
ConversationEntryDTO systemEntry = new ConversationEntryDTO(
|
||||||
|
ConversationEntryEntity.SISTEMA,
|
||||||
|
ConversationEntryType.INICIO,
|
||||||
|
Instant.now(),
|
||||||
|
"Pantalla :" + externalRequest.pantallaContexto() + " Agregada a la conversacion :",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
return persistConversationTurn(userId, sessionId, systemEntry, userPhoneNumber, externalRequest.pantallaContexto())
|
||||||
|
.then(quickReplyContentService.getQuickReplies(externalRequest.pantallaContexto()))
|
||||||
|
.map(quickReplyDTO -> new DetectIntentResponseDTO(sessionId, null, quickReplyDTO));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<DetectIntentResponseDTO> manageConversation(ExternalConvRequestDTO 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 memoryStoreConversationService.getSessionByTelefono(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 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())
|
||||||
|
.thenReturn(new DetectIntentResponseDTO(sessionId, null, quickReplyDTO));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.quickreplies.QuestionDTO;
|
||||||
|
import com.example.dto.quickreplies.QuickReplyDTO;
|
||||||
|
import com.google.cloud.firestore.DocumentSnapshot;
|
||||||
|
import com.google.cloud.firestore.Firestore;
|
||||||
|
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")
|
||||||
|
.document("default-app-id")
|
||||||
|
.collection("quick-replies")
|
||||||
|
.document(collectionId)
|
||||||
|
.get()
|
||||||
|
.get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(DocumentSnapshot::exists)
|
||||||
|
.map(document -> {
|
||||||
|
QuestionDTO pregunta = new QuestionDTO(document.getString("titulo"), document.getString("descripcion"));
|
||||||
|
return new QuickReplyDTO("preguntas sobre " + collectionId, List.of(pregunta));
|
||||||
|
})
|
||||||
|
.doOnSuccess(quickReplyDTO -> {
|
||||||
|
if (quickReplyDTO != null) {
|
||||||
|
logger.info("Successfully fetched {} quick replies for document: {}", quickReplyDTO.preguntas().size(), collectionId);
|
||||||
|
} else {
|
||||||
|
logger.info("No quick reply document found for id: {}", collectionId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnError(error -> logger.error("Error fetching quick replies from Firestore for document: {}", collectionId, error))
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
logger.info("No quick reply document found for id: {}", collectionId);
|
||||||
|
return Mono.empty();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/main/java/com/example/util/FirestoreDataImporter.java
Normal file
80
src/main/java/com/example/util/FirestoreDataImporter.java
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
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 org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class FirestoreDataImporter {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FirestoreDataImporter.class);
|
||||||
|
private static final String QUICK_REPLIES_COLLECTION_PATH_FORMAT = "artifacts/%s/quick-replies";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FirestoreBaseRepository firestoreBaseRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void importDataOnStartup() {
|
||||||
|
try {
|
||||||
|
importQuickReplies();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to import data to Firestore on startup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importQuickReplies() throws IOException, ExecutionException, InterruptedException {
|
||||||
|
String collectionPath = String.format(QUICK_REPLIES_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
||||||
|
|
||||||
|
importJson(collectionPath, "home");
|
||||||
|
importJson(collectionPath, "pagos");
|
||||||
|
importJson(collectionPath, "finanzas");
|
||||||
|
importJson(collectionPath, "lealtad");
|
||||||
|
importJson(collectionPath, "descubre");
|
||||||
|
importJson(collectionPath, "detalle-tdc");
|
||||||
|
importJson(collectionPath, "detalle-tdd");
|
||||||
|
importJson(collectionPath, "transferencia");
|
||||||
|
importJson(collectionPath, "retiro-sin-tarjeta");
|
||||||
|
importJson(collectionPath, "capsulas");
|
||||||
|
importJson(collectionPath, "inversiones");
|
||||||
|
importJson(collectionPath, "prestamos");
|
||||||
|
|
||||||
|
logger.info("All JSON files were imported successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importJson(String collectionPath, String documentId) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
String resourcePath = "/quick-replies/" + documentId + ".json";
|
||||||
|
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
|
||||||
|
if (inputStream == null) {
|
||||||
|
logger.warn("Resource not found: {}", resourcePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> data = objectMapper.readValue(inputStream, new TypeReference<Map<String, Object>>() {});
|
||||||
|
DocumentReference docRef = firestoreBaseRepository.getDocumentReference(collectionPath, documentId);
|
||||||
|
if (!firestoreBaseRepository.documentExists(docRef)) {
|
||||||
|
firestoreBaseRepository.setDocument(docRef, data);
|
||||||
|
logger.debug("Successfully imported {} to Firestore.", documentId);
|
||||||
|
} else {
|
||||||
|
logger.debug("{} already exists in Firestore. Skipping import.", documentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/main/resources/application-dev.properties
Normal file
62
src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Spring Boot Configuration Template
|
||||||
|
# =========================================
|
||||||
|
# This file serves as a reference template for all application configuration properties.
|
||||||
|
|
||||||
|
# Best Practices:
|
||||||
|
# - Use Spring Profiles (e.g., application-dev.properties, application-prod.properties)
|
||||||
|
# to manage environment-specific settings.
|
||||||
|
# - Do not store in PROD sensitive information (e.g., API keys, passwords) directly here.
|
||||||
|
# Use environment variables or a configuration server for production environments.
|
||||||
|
# - This template can be adapted for logging configuration, database connections,
|
||||||
|
# and other external service settings.
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Orchestrator general Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.cloud.gcp.project-id=bnt-orquestador-cognitivo-dev
|
||||||
|
# =========================================================
|
||||||
|
# Google Firestore Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.cloud.gcp.firestore.project-id=bnt-orquestador-cognitivo-dev
|
||||||
|
spring.cloud.gcp.firestore.database-id=bnt-orquestador-cognitivo-firestore-bdo-dev
|
||||||
|
spring.cloud.gcp.firestore.host=firestore.googleapis.com
|
||||||
|
spring.cloud.gcp.firestore.port=443
|
||||||
|
# =========================================================
|
||||||
|
# Google Memorystore(Redis) Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.data.redis.host=10.33.22.4
|
||||||
|
spring.data.redis.port=6379
|
||||||
|
spring.redis.jedis.pool.enabled=true
|
||||||
|
spring.redis.notify-keyspace-events=Ex
|
||||||
|
#spring.data.redis.password=23cb4c76-9d96-4c74-b8c0-778fb364877a
|
||||||
|
#spring.data.redis.username=default
|
||||||
|
|
||||||
|
# SSL Configuration (if using SSL)
|
||||||
|
# spring.data.redis.ssl=true
|
||||||
|
# spring.data.redis.ssl.key-store=classpath:keystore.p12
|
||||||
|
# spring.data.redis.ssl.key-store-password=your-keystore-password
|
||||||
|
# =========================================================
|
||||||
|
# Google Conversational Agents Configuration
|
||||||
|
# =========================================================
|
||||||
|
dialogflow.cx.project-id=bnt-orquestador-cognitivo-dev
|
||||||
|
dialogflow.cx.location=us-central1
|
||||||
|
dialogflow.cx.agent-id=5590ff1d-1f66-4777-93f5-1a608f1900ac
|
||||||
|
dialogflow.default-language-code=es
|
||||||
|
# =========================================================
|
||||||
|
# Google Generative AI (Gemini) Configuration
|
||||||
|
# =========================================================
|
||||||
|
google.cloud.project=bnt-orquestador-cognitivo-dev
|
||||||
|
google.cloud.location=us-central1
|
||||||
|
gemini.model.name=gemini-2.0-flash-001
|
||||||
|
# =========================================================
|
||||||
|
# (Gemini) MessageFilter Configuration
|
||||||
|
# =========================================================
|
||||||
|
messagefilter.geminimodel=gemini-2.0-flash-001
|
||||||
|
messagefilter.temperature=0.1f
|
||||||
|
messagefilter.maxOutputTokens=800
|
||||||
|
messagefilter.topP= 0.1f
|
||||||
|
messagefilter.prompt=prompts/message_filter_prompt.txt
|
||||||
60
src/main/resources/application-prod.properties
Normal file
60
src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Spring Boot Configuration Template
|
||||||
|
# =========================================
|
||||||
|
# This file serves as a reference template for all application configuration properties.
|
||||||
|
|
||||||
|
# Best Practices:
|
||||||
|
# - Use Spring Profiles (e.g., application-dev.properties, application-prod.properties)
|
||||||
|
# to manage environment-specific settings.
|
||||||
|
# - Do not store in PROD sensitive information (e.g., API keys, passwords) directly here.
|
||||||
|
# Use environment variables or a configuration server for production environments.
|
||||||
|
# - This template can be adapted for logging configuration, database connections,
|
||||||
|
# and other external service settings.
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Orchestrator general Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.cloud.gcp.project-id=${GCP_PROJECT_ID}
|
||||||
|
# =========================================================
|
||||||
|
# Google Firestore Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.cloud.gcp.firestore.project-id=${GCP_PROJECT_ID}
|
||||||
|
spring.cloud.gcp.firestore.database-id=${GCP_FIRESTORE_DATABASE_ID}
|
||||||
|
spring.cloud.gcp.firestore.host=${GCP_FIRESTORE_HOST}
|
||||||
|
spring.cloud.gcp.firestore.port=${GCP_FIRESTORE_PORT}
|
||||||
|
# =========================================================
|
||||||
|
# Google Memorystore(Redis) Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.data.redis.host=${REDIS_HOST}
|
||||||
|
spring.data.redis.port=${REDIS_PORT}
|
||||||
|
#spring.data.redis.password=23cb4c76-9d96-4c74-b8c0-778fb364877a
|
||||||
|
#spring.data.redis.username=default
|
||||||
|
|
||||||
|
# SSL Configuration (if using SSL)
|
||||||
|
# spring.data.redis.ssl=true
|
||||||
|
# spring.data.redis.ssl.key-store=classpath:keystore.p12
|
||||||
|
# spring.data.redis.ssl.key-store-password=your-keystore-password
|
||||||
|
# =========================================================
|
||||||
|
# Google Conversational Agents Configuration
|
||||||
|
# =========================================================
|
||||||
|
dialogflow.cx.project-id=${DIALOGFLOW_CX_PROJECT_ID}
|
||||||
|
dialogflow.cx.location=${DIALOGFLOW_CX_LOCATION}
|
||||||
|
dialogflow.cx.agent-id=${DIALOGFLOW_CX_AGENT_ID}
|
||||||
|
dialogflow.default-language-code=${DIALOGFLOW_DEFAULT_LANGUAGE_CODE}
|
||||||
|
# =========================================================
|
||||||
|
# Google Generative AI (Gemini) Configuration
|
||||||
|
# =========================================================
|
||||||
|
google.cloud.project=${GCP_PROJECT_ID}
|
||||||
|
google.cloud.location=${GCP_LOCATION}
|
||||||
|
gemini.model.name=${GEMINI_MODEL_NAME}
|
||||||
|
# =========================================================
|
||||||
|
# (Gemini) MessageFilter Configuration
|
||||||
|
# =========================================================
|
||||||
|
messagefilter.geminimodel=${MESSAGE_FILTER_GEMINI_MODEL}
|
||||||
|
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
|
||||||
60
src/main/resources/application-qa.properties
Normal file
60
src/main/resources/application-qa.properties
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Spring Boot Configuration Template
|
||||||
|
# =========================================
|
||||||
|
# This file serves as a reference template for all application configuration properties.
|
||||||
|
|
||||||
|
# Best Practices:
|
||||||
|
# - Use Spring Profiles (e.g., application-dev.properties, application-prod.properties)
|
||||||
|
# to manage environment-specific settings.
|
||||||
|
# - Do not store in PROD sensitive information (e.g., API keys, passwords) directly here.
|
||||||
|
# Use environment variables or a configuration server for production environments.
|
||||||
|
# - This template can be adapted for logging configuration, database connections,
|
||||||
|
# and other external service settings.
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Orchestrator general Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.cloud.gcp.project-id=${GCP_PROJECT_ID}
|
||||||
|
# =========================================================
|
||||||
|
# Google Firestore Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.cloud.gcp.firestore.project-id=${GCP_PROJECT_ID}
|
||||||
|
spring.cloud.gcp.firestore.database-id=${GCP_FIRESTORE_DATABASE_ID}
|
||||||
|
spring.cloud.gcp.firestore.host=${GCP_FIRESTORE_HOST}
|
||||||
|
spring.cloud.gcp.firestore.port=${GCP_FIRESTORE_PORT}
|
||||||
|
# =========================================================
|
||||||
|
# Google Memorystore(Redis) Configuration
|
||||||
|
# =========================================================
|
||||||
|
spring.data.redis.host=${REDIS_HOST}
|
||||||
|
spring.data.redis.port=${REDIS_PORT}
|
||||||
|
#spring.data.redis.password=23cb4c76-9d96-4c74-b8c0-778fb364877a
|
||||||
|
#spring.data.redis.username=default
|
||||||
|
|
||||||
|
# SSL Configuration (if using SSL)
|
||||||
|
# spring.data.redis.ssl=true
|
||||||
|
# spring.data.redis.ssl.key-store=classpath:keystore.p12
|
||||||
|
# spring.data.redis.ssl.key-store-password=your-keystore-password
|
||||||
|
# =========================================================
|
||||||
|
# Google Conversational Agents Configuration
|
||||||
|
# =========================================================
|
||||||
|
dialogflow.cx.project-id=${DIALOGFLOW_CX_PROJECT_ID}
|
||||||
|
dialogflow.cx.location=${DIALOGFLOW_CX_LOCATION}
|
||||||
|
dialogflow.cx.agent-id=${DIALOGFLOW_CX_AGENT_ID}
|
||||||
|
dialogflow.default-language-code=${DIALOGFLOW_DEFAULT_LANGUAGE_CODE}
|
||||||
|
# =========================================================
|
||||||
|
# Google Generative AI (Gemini) Configuration
|
||||||
|
# =========================================================
|
||||||
|
google.cloud.project=${GCP_PROJECT_ID}
|
||||||
|
google.cloud.location=${GCP_LOCATION}
|
||||||
|
gemini.model.name=${GEMINI_MODEL_NAME}
|
||||||
|
# =========================================================
|
||||||
|
# (Gemini) MessageFilter Configuration
|
||||||
|
# =========================================================
|
||||||
|
messagefilter.geminimodel=${MESSAGE_FILTER_GEMINI_MODEL}
|
||||||
|
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
|
||||||
@@ -1,64 +1 @@
|
|||||||
# Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
spring.profiles.active=dev
|
||||||
# Your use of it is subject to your agreement with Google.
|
|
||||||
|
|
||||||
# =========================================
|
|
||||||
# Spring Boot Configuration Template
|
|
||||||
# =========================================
|
|
||||||
# This file serves as a reference template for all application configuration properties.
|
|
||||||
|
|
||||||
# Best Practices:
|
|
||||||
# - Use Spring Profiles (e.g., application-dev.properties, application-prod.properties)
|
|
||||||
# to manage environment-specific settings.
|
|
||||||
# - Do not store in PROD sensitive information (e.g., API keys, passwords) directly here.
|
|
||||||
# Use environment variables or a configuration server for production environments.
|
|
||||||
# - This template can be adapted for logging configuration, database connections,
|
|
||||||
# and other external service settings.
|
|
||||||
|
|
||||||
# =========================================================
|
|
||||||
# Orchestrator general Configuration
|
|
||||||
# =========================================================
|
|
||||||
spring.cloud.gcp.project-id=bnt-orquestador-cognitivo-dev
|
|
||||||
|
|
||||||
# Firestore Emulator Configuration (for local development)
|
|
||||||
#spring.cloud.gcp.firestore.emulator-host=localhost:8080
|
|
||||||
spring.cloud.gcp.firestore.emulator.enabled=false
|
|
||||||
# =========================================================
|
|
||||||
# Google Firestore Configuration
|
|
||||||
# =========================================================
|
|
||||||
spring.cloud.gcp.firestore.project-id=bnt-orquestador-cognitivo-dev
|
|
||||||
spring.cloud.gcp.firestore.database-id=bnt-orquestador-cognitivo-firestore-bdo-dev
|
|
||||||
spring.cloud.gcp.firestore.host=firestore.googleapis.com
|
|
||||||
spring.cloud.gcp.firestore.port=443
|
|
||||||
# =========================================================
|
|
||||||
# Google Memorystore(Redis) Configuration
|
|
||||||
# =========================================================
|
|
||||||
spring.data.redis.host=10.33.22.4
|
|
||||||
spring.data.redis.port=6379
|
|
||||||
#spring.data.redis.password=23cb4c76-9d96-4c74-b8c0-778fb364877a
|
|
||||||
#spring.data.redis.username=default
|
|
||||||
|
|
||||||
# SSL Configuration (if using SSL)
|
|
||||||
# spring.data.redis.ssl=true
|
|
||||||
# spring.data.redis.ssl.key-store=classpath:keystore.p12
|
|
||||||
# spring.data.redis.ssl.key-store-password=your-keystore-password
|
|
||||||
# =========================================================
|
|
||||||
# Google Conversational Agents Configuration
|
|
||||||
# =========================================================
|
|
||||||
dialogflow.cx.project-id=bnt-orquestador-cognitivo-dev
|
|
||||||
dialogflow.cx.location=us-central1
|
|
||||||
dialogflow.cx.agent-id=5590ff1d-1f66-4777-93f5-1a608f1900ac
|
|
||||||
dialogflow.default-language-code=es
|
|
||||||
# =========================================================
|
|
||||||
# Google Generative AI (Gemini) Configuration
|
|
||||||
# =========================================================
|
|
||||||
google.cloud.project=bnt-orquestador-cognitivo-dev
|
|
||||||
google.cloud.location=us-central1
|
|
||||||
gemini.model.name=gemini-2.0-flash-001
|
|
||||||
# =========================================================
|
|
||||||
# (Gemini) MessageFilter Configuration
|
|
||||||
# =========================================================
|
|
||||||
messagefilter.geminimodel=gemini-2.0-flash-001
|
|
||||||
messagefilter.temperature=0.1f
|
|
||||||
messagefilter.maxOutputTokens=800
|
|
||||||
messagefilter.topP= 0.1f
|
|
||||||
messagefilter.prompt=prompts/message_filter_prompt.txt
|
|
||||||
1
src/main/resources/quick-replies/capsulas.json
Normal file
1
src/main/resources/quick-replies/capsulas.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Capsulas"}
|
||||||
1
src/main/resources/quick-replies/descubre.json
Normal file
1
src/main/resources/quick-replies/descubre.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Descubre"}
|
||||||
1
src/main/resources/quick-replies/detalle-tdc.json
Normal file
1
src/main/resources/quick-replies/detalle-tdc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Detalle TDC"}
|
||||||
1
src/main/resources/quick-replies/detalle-tdd.json
Normal file
1
src/main/resources/quick-replies/detalle-tdd.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Detalle TDD"}
|
||||||
1
src/main/resources/quick-replies/finanzas.json
Normal file
1
src/main/resources/quick-replies/finanzas.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Finanzas"}
|
||||||
1
src/main/resources/quick-replies/home.json
Normal file
1
src/main/resources/quick-replies/home.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Home"}
|
||||||
1
src/main/resources/quick-replies/inversiones.json
Normal file
1
src/main/resources/quick-replies/inversiones.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Inversiones"}
|
||||||
1
src/main/resources/quick-replies/lealtad.json
Normal file
1
src/main/resources/quick-replies/lealtad.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Lealtad"}
|
||||||
4
src/main/resources/quick-replies/pagos.json
Normal file
4
src/main/resources/quick-replies/pagos.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"titulo": "Payment History",
|
||||||
|
"descripcion": "View your recent payments"
|
||||||
|
}
|
||||||
1
src/main/resources/quick-replies/prestamos.json
Normal file
1
src/main/resources/quick-replies/prestamos.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Prestamos"}
|
||||||
1
src/main/resources/quick-replies/retiro-sin-tarjeta.json
Normal file
1
src/main/resources/quick-replies/retiro-sin-tarjeta.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Retiro sin tarjeta"}
|
||||||
1
src/main/resources/quick-replies/transferencia.json
Normal file
1
src/main/resources/quick-replies/transferencia.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"titulo": "Transferencia"}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
* 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.
|
* Your use of it is subject to your agreement with Google.
|
||||||
*/
|
*/
|
||||||
package com.example.service;
|
package com.example.service.integration_testing;
|
||||||
|
|
||||||
import com.example.service.base.MessageEntryFilter;
|
import com.example.service.base.MessageEntryFilter;
|
||||||
import com.example.util.PerformanceTimer;
|
import com.example.util.PerformanceTimer;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.service;
|
package com.example.service.unit_testing;
|
||||||
|
|
||||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
* 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.
|
* Your use of it is subject to your agreement with Google.
|
||||||
*/
|
*/
|
||||||
package com.example.service;
|
package com.example.service.unit_testing;
|
||||||
|
|
||||||
import com.example.service.base.GeminiClientService;
|
import com.example.service.base.GeminiClientService;
|
||||||
import com.example.service.base.MessageEntryFilter;
|
import com.example.service.base.MessageEntryFilter;
|
||||||
Reference in New Issue
Block a user