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