UPDATE 12-ago-2025
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.google.genai.Client;
|
||||
import com.google.genai.errors.GenAiIOException;
|
||||
import com.google.genai.types.Content;
|
||||
import com.google.genai.types.GenerateContentConfig;
|
||||
import com.google.genai.types.GenerateContentResponse;
|
||||
import com.google.genai.types.Part;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class GeminiClientService {
|
||||
|
||||
private final Client geminiClient;
|
||||
|
||||
public GeminiClientService(Client geminiClient) {
|
||||
this.geminiClient = geminiClient;
|
||||
}
|
||||
|
||||
public String generateContent(String prompt, Float temperature, Integer maxOutputTokens, String modelName) {
|
||||
try {
|
||||
Content content = Content.fromParts(Part.fromText(prompt));
|
||||
GenerateContentConfig config = GenerateContentConfig.builder()
|
||||
.temperature(temperature)
|
||||
.maxOutputTokens(maxOutputTokens)
|
||||
.build();
|
||||
|
||||
GenerateContentResponse response = geminiClient.models.generateContent(modelName, content, config);
|
||||
|
||||
if (response != null && response.text() != null) {
|
||||
return response.text();
|
||||
} else {
|
||||
return "No content generated or unexpected response structure.";
|
||||
}
|
||||
} catch (GenAiIOException e) {
|
||||
System.err.println("Gemini API I/O error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return "Error: An API communication issue occurred: " + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
System.err.println("An unexpected error occurred during Gemini content generation: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return "Error: An unexpected issue occurred during content generation.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.service.base;
|
||||
|
||||
public class ConvSessionCloserService {
|
||||
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
package com.example.service;
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.DetectIntentResponseDTO;
|
||||
import com.example.mapper.DialogflowRequestMapper;
|
||||
import com.example.mapper.DialogflowResponseMapper;
|
||||
package com.example.service.base;
|
||||
|
||||
import com.example.mapper.conversation.DialogflowRequestMapper;
|
||||
import com.example.mapper.conversation.DialogflowResponseMapper;
|
||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.exception.DialogflowClientException;
|
||||
|
||||
import com.google.api.gax.rpc.ApiException;
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
|
||||
import com.google.cloud.dialogflow.cx.v3.SessionsClient;
|
||||
import com.google.cloud.dialogflow.cx.v3.SessionName;
|
||||
import com.google.cloud.dialogflow.cx.v3.SessionsSettings;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Service for interacting with the Dialogflow CX API to detect user DetectIntent.
|
||||
* It encapsulates the low-level API calls, handling request mapping from DTOs,
|
||||
* managing the `SessionsClient`, and translating API responses into DTOs,
|
||||
* all within a reactive programming context.
|
||||
*/
|
||||
@Service
|
||||
public class DialogflowClientService {
|
||||
|
||||
@@ -57,6 +64,7 @@ public class DialogflowClientService {
|
||||
.build();
|
||||
this.sessionsClient = SessionsClient.create(sessionsSettings);
|
||||
logger.info("Dialogflow CX SessionsClient initialized successfully for endpoint: {}", regionalEndpoint);
|
||||
logger.info("Dialogflow CX SessionsClient initialized successfully for agent : {}", dialogflowCxAgentId);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create Dialogflow CX SessionsClient: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
@@ -100,7 +108,6 @@ public class DialogflowClientService {
|
||||
|
||||
// Build the final DetectIntentRequest Protobuf object
|
||||
DetectIntentRequest detectIntentRequest = detectIntentRequestBuilder.build();
|
||||
|
||||
return Mono.fromCallable(() -> {
|
||||
logger.debug("Calling Dialogflow CX detectIntent for session: {}", sessionId);
|
||||
return sessionsClient.detectIntent(detectIntentRequest);
|
||||
@@ -111,10 +118,6 @@ public class DialogflowClientService {
|
||||
return new DialogflowClientException(
|
||||
"Dialogflow CX API error: " + e.getStatusCode().getCode() + " - " + e.getMessage(), e);
|
||||
})
|
||||
.onErrorMap(IOException.class, e -> {
|
||||
logger.error("IO error when calling Dialogflow CX for session {}: {}", sessionId, e.getMessage(),e);
|
||||
return new RuntimeException("IO error with Dialogflow CX API: " + e.getMessage(), e);
|
||||
})
|
||||
.map(dfResponse -> this.dialogflowResponseMapper.mapFromDialogflowResponse(dfResponse, sessionId));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
package com.example.service.base;
|
||||
|
||||
import com.example.exception.GeminiClientException;
|
||||
import com.google.genai.Client;
|
||||
import com.google.genai.errors.GenAiIOException;
|
||||
import com.google.genai.types.Content;
|
||||
import com.google.genai.types.GenerateContentConfig;
|
||||
import com.google.genai.types.GenerateContentResponse;
|
||||
import com.google.genai.types.Part;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Service for interacting with the Gemini API to generate content.
|
||||
* It encapsulates the low-level API calls, handling prompt configuration,
|
||||
* and error management to provide a clean and robust content generation interface.
|
||||
*/
|
||||
@Service
|
||||
public class GeminiClientService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GeminiClientService.class);
|
||||
private final Client geminiClient;
|
||||
|
||||
public GeminiClientService(Client geminiClient) {
|
||||
this.geminiClient = geminiClient;
|
||||
}
|
||||
|
||||
public String generateContent(String prompt, Float temperature, Integer maxOutputTokens, String modelName,Float topP) throws GeminiClientException {
|
||||
try {
|
||||
Content content = Content.fromParts(Part.fromText(prompt));
|
||||
GenerateContentConfig config = GenerateContentConfig.builder()
|
||||
.temperature(temperature)
|
||||
.maxOutputTokens(maxOutputTokens)
|
||||
.topP(topP)
|
||||
.build();
|
||||
|
||||
logger.debug("Sending request to Gemini model '{}'", modelName);
|
||||
GenerateContentResponse response = geminiClient.models.generateContent(modelName, content, config);
|
||||
|
||||
if (response != null && response.text() != null) {
|
||||
return response.text();
|
||||
} else {
|
||||
logger.warn("Gemini returned no content or an unexpected response structure for model '{}'.", modelName);
|
||||
throw new GeminiClientException("No content generated or unexpected response structure.");
|
||||
}
|
||||
} catch (GenAiIOException e) {
|
||||
logger.error("Gemini API I/O error while calling model '{}': {}", modelName, e.getMessage(), e);
|
||||
throw new GeminiClientException("An API communication issue occurred: " + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
logger.error("An unexpected error occurred during Gemini content generation for model '{}': {}", modelName, e.getMessage(), e);
|
||||
throw new GeminiClientException("An unexpected issue occurred during content generation.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/main/java/com/example/service/base/MessageEntryFilter.java
Normal file
125
src/main/java/com/example/service/base/MessageEntryFilter.java
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.base;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Service to classify message entries (user text input from DetectIntent) into predefined categories
|
||||
* like "CONVERSATION" or "NOTIFICATION" using Gemini model.
|
||||
*/
|
||||
@Service
|
||||
public class MessageEntryFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageEntryFilter.class);
|
||||
private final GeminiClientService geminiService;
|
||||
|
||||
@Value("${messagefilter.geminimodel:gemini-2.0-flash-001}")
|
||||
private String geminiModelNameClassifier;
|
||||
|
||||
@Value("${messagefilter.temperature:0.1f}")
|
||||
private Float classifierTemperature;
|
||||
|
||||
@Value("${messagefilter.maxOutputTokens:10}")
|
||||
private Integer classifierMaxOutputTokens;
|
||||
|
||||
@Value("${messagefilter.topP:0.1f}")
|
||||
private Float classifierTopP;
|
||||
|
||||
@Value("${messagefilter.prompt:prompts/message_filter_prompt.txt}")
|
||||
private String promptFilePath;
|
||||
|
||||
public static final String CATEGORY_CONVERSATION = "CONVERSATION";
|
||||
public static final String CATEGORY_NOTIFICATION = "NOTIFICATION";
|
||||
public static final String CATEGORY_UNKNOWN = "UNKNOWN";
|
||||
public static final String CATEGORY_ERROR = "ERROR";
|
||||
|
||||
private String promptTemplate;
|
||||
|
||||
public MessageEntryFilter(GeminiClientService geminiService) {
|
||||
this.geminiService = Objects.requireNonNull(geminiService, "GeminiClientService cannot be null for MessageEntryFilter.");
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void loadPromptTemplate() {
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource(promptFilePath);
|
||||
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
this.promptTemplate = FileCopyUtils.copyToString(reader);
|
||||
}
|
||||
logger.info("Successfully loaded prompt template from '{}'.", promptFilePath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load prompt template from '{}'. Please ensure the file exists.", promptFilePath, e);
|
||||
throw new IllegalStateException("Could not load prompt template.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String classifyMessage(String queryInputText, String notificationsJson, String conversationJson) {
|
||||
if (queryInputText == null || queryInputText.isBlank()) {
|
||||
logger.warn("Query input text for classification is null or blank. Returning {}.", CATEGORY_UNKNOWN);
|
||||
return CATEGORY_UNKNOWN;
|
||||
}
|
||||
|
||||
String interruptingNotification = (notificationsJson != null && !notificationsJson.isBlank()) ?
|
||||
notificationsJson : "No interrupting notification.";
|
||||
|
||||
String conversationHistory = (conversationJson != null && !conversationJson.isBlank()) ?
|
||||
conversationJson : "No conversation history.";
|
||||
|
||||
String classificationPrompt = String.format(
|
||||
this.promptTemplate,
|
||||
CATEGORY_NOTIFICATION, CATEGORY_NOTIFICATION, CATEGORY_CONVERSATION, CATEGORY_CONVERSATION,
|
||||
CATEGORY_CONVERSATION, CATEGORY_CONVERSATION, CATEGORY_CONVERSATION, CATEGORY_NOTIFICATION,
|
||||
CATEGORY_NOTIFICATION, CATEGORY_CONVERSATION, CATEGORY_CONVERSATION,
|
||||
conversationHistory,
|
||||
interruptingNotification,
|
||||
queryInputText
|
||||
);
|
||||
|
||||
logger.info("Sending classification request to Gemini for input (first 100 chars): '{}'...",
|
||||
queryInputText.substring(0, Math.min(queryInputText.length(), 100)));
|
||||
|
||||
try {
|
||||
String geminiResponse = geminiService.generateContent(
|
||||
classificationPrompt,
|
||||
classifierTemperature,
|
||||
classifierMaxOutputTokens,
|
||||
geminiModelNameClassifier,
|
||||
classifierTopP
|
||||
);
|
||||
|
||||
String resultCategory = switch (geminiResponse != null ? geminiResponse.trim().toUpperCase() : "") {
|
||||
case CATEGORY_CONVERSATION -> {
|
||||
logger.info("Classified as {}. Input: '{}'", CATEGORY_CONVERSATION, queryInputText);
|
||||
yield CATEGORY_CONVERSATION;
|
||||
}
|
||||
case CATEGORY_NOTIFICATION -> {
|
||||
logger.info("Classified as {}. Input: '{}'", CATEGORY_NOTIFICATION, queryInputText);
|
||||
yield CATEGORY_NOTIFICATION;
|
||||
}
|
||||
default -> {
|
||||
logger.warn("Gemini returned an unrecognised classification or was null/blank: '{}'. Expected '{}' or '{}'. Input: '{}'. Returning {}.",
|
||||
geminiResponse, CATEGORY_CONVERSATION, CATEGORY_NOTIFICATION, queryInputText, CATEGORY_UNKNOWN);
|
||||
yield CATEGORY_UNKNOWN;
|
||||
}
|
||||
};
|
||||
return resultCategory;
|
||||
} catch (Exception e) {
|
||||
logger.error("An error occurred during Gemini content generation for message classification.", e);
|
||||
return CATEGORY_UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,49 @@
|
||||
package com.example.service;
|
||||
/*
|
||||
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
||||
* Your use of it is subject to your agreement with Google.
|
||||
*/
|
||||
|
||||
package com.example.service.conversation;
|
||||
|
||||
import com.example.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.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.conversation.ConversationContext;
|
||||
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||
import com.example.dto.dialogflow.conversation.ExternalConvRequestDTO;
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.DetectIntentResponseDTO;
|
||||
import com.example.dto.base.ConversationContext;
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
import com.example.dto.base.UsuarioDTO;
|
||||
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.UUID;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
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
|
||||
public class ConversationManagerService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConversationManagerService.class);
|
||||
|
||||
private static final long SESSION_RESET_THRESHOLD_HOURS = 24;
|
||||
private static final String CURRENT_PAGE_PARAM = "currentPage";
|
||||
private final ExternalConvRequestMapper externalRequestToDialogflowMapper;
|
||||
|
||||
private final DialogflowClientService dialogflowServiceClient;
|
||||
private final FirestoreConversationService firestoreConversationService;
|
||||
private final MemoryStoreConversationService memoryStoreConversationService;
|
||||
@@ -27,13 +51,24 @@ public class ConversationManagerService {
|
||||
public ConversationManagerService(
|
||||
DialogflowClientService dialogflowServiceClient,
|
||||
FirestoreConversationService firestoreConversationService,
|
||||
MemoryStoreConversationService memoryStoreConversationService) {
|
||||
MemoryStoreConversationService memoryStoreConversationService,
|
||||
ExternalConvRequestMapper externalRequestToDialogflowMapper) {
|
||||
this.dialogflowServiceClient = dialogflowServiceClient;
|
||||
this.firestoreConversationService = firestoreConversationService;
|
||||
this.memoryStoreConversationService = memoryStoreConversationService;
|
||||
this.externalRequestToDialogflowMapper = externalRequestToDialogflowMapper;
|
||||
}
|
||||
|
||||
public Mono<DetectIntentResponseDTO> manageConversation(DetectIntentRequestDTO request) {
|
||||
public Mono<DetectIntentResponseDTO>manageConversation(ExternalConvRequestDTO Externalrequest) {
|
||||
final DetectIntentRequestDTO request;
|
||||
try {
|
||||
request = externalRequestToDialogflowMapper.mapExternalRequestToDetectIntentRequest(Externalrequest);
|
||||
logger.debug("Successfully pre-mapped ExternalRequestDTO to DetectIntentRequestDTO");
|
||||
} catch (IllegalArgumentException e) {
|
||||
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));
|
||||
}
|
||||
|
||||
final ConversationContext context;
|
||||
try {
|
||||
context = resolveAndValidateRequest(request);
|
||||
@@ -47,40 +82,48 @@ public class ConversationManagerService {
|
||||
final String userPhoneNumber = context.primaryPhoneNumber();
|
||||
|
||||
Mono<ConversationSessionDTO> sessionMono;
|
||||
if (userPhoneNumber != null && !userPhoneNumber.trim().isEmpty()) {
|
||||
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 = UUID.randomUUID().toString();
|
||||
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 {
|
||||
String newSessionId = UUID.randomUUID().toString();
|
||||
logger.warn("No phone number provided in request. Creating new session: {}", newSessionId);
|
||||
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 sessionMono.flatMap(session -> {
|
||||
final String finalSessionId = session.sessionId();
|
||||
|
||||
logger.info("Managing conversation for resolved session: {}", finalSessionId);
|
||||
|
||||
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText);
|
||||
DetectIntentRequestDTO updatedRequest = request.withSessionId(finalSessionId);
|
||||
|
||||
final DetectIntentRequestDTO requestToDialogflow;
|
||||
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 {
|
||||
requestToDialogflow = request;
|
||||
}
|
||||
|
||||
return this.persistConversationTurn(userId, finalSessionId, userEntry, userPhoneNumber)
|
||||
.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))
|
||||
// After user entry persistence is complete (Mono<Void> emits 'onComplete'),
|
||||
// then proceed to call Dialogflow.
|
||||
.then(Mono.defer(() -> { // Use Mono.defer to ensure Dialogflow call is subscribed AFTER persistence
|
||||
// Call Dialogflow.
|
||||
return dialogflowServiceClient.detectIntent(finalSessionId, updatedRequest)
|
||||
.then(Mono.defer(() -> {
|
||||
return dialogflowServiceClient.detectIntent(finalSessionId, requestToDialogflow)
|
||||
.doOnSuccess(response -> {
|
||||
logger.debug("Received Dialogflow CX response for session {}. Initiating agent response persistence.", finalSessionId);
|
||||
ConversationEntryDTO agentEntry = ConversationEntryDTO.forAgent(response.queryResult());
|
||||
// Agent entry persistence can still be backgrounded via .subscribe()
|
||||
// if its completion isn't strictly required before returning the Dialogflow response.
|
||||
this.persistConversationTurn(userId, finalSessionId, agentEntry, userPhoneNumber).subscribe(
|
||||
v -> logger.debug("Background: Agent entry persistence initiated for session {}.", finalSessionId),
|
||||
e -> logger.error("Background: Error during agent entry persistence for session {}: {}", finalSessionId, e.getMessage(), e)
|
||||
@@ -113,20 +156,38 @@ public class ConversationManagerService {
|
||||
.doOnError(e -> logger.error("Error during primary Redis write for session {}. Type: {}: {}", sessionId, entry.type().name(), e.getMessage(), e));
|
||||
}
|
||||
|
||||
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
|
||||
String primaryPhoneNumber = Optional.ofNullable(request.usuario())
|
||||
.map(UsuarioDTO::telefono)
|
||||
.orElse(null);
|
||||
|
||||
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
|
||||
Map<String, Object> params = Optional.ofNullable(request.queryParams())
|
||||
.map(queryParamsDTO -> queryParamsDTO.parameters())
|
||||
.orElse(Collections.emptyMap());
|
||||
|
||||
String primaryPhoneNumber = null;
|
||||
Object telefonoObj = params.get("telefono"); // Get from map
|
||||
if (telefonoObj instanceof String) {
|
||||
primaryPhoneNumber = (String) telefonoObj;
|
||||
} else if (telefonoObj != null) {
|
||||
logger.warn("Parameter 'telefono' in queryParams is not a String (type: {}). Expected String.", telefonoObj.getClass().getName());
|
||||
}
|
||||
|
||||
if (primaryPhoneNumber == null || primaryPhoneNumber.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Phone number (telefono) is required in the 'usuario' field for conversation management.");
|
||||
throw new IllegalArgumentException("Phone number (telefono) is required in query parameters for conversation management.");
|
||||
}
|
||||
String resolvedUserId = request.userId();
|
||||
|
||||
String resolvedUserId = null;
|
||||
Object userIdObj = params.get("usuario_id");
|
||||
if (userIdObj instanceof String) {
|
||||
resolvedUserId = (String) userIdObj;
|
||||
} else if (userIdObj != null) {
|
||||
logger.warn("Parameter 'userId' in queryParams is not a String (type: {}). Expected String.", userIdObj.getClass().getName());
|
||||
}
|
||||
|
||||
if (resolvedUserId == null || resolvedUserId.trim().isEmpty()) {
|
||||
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", ""); // Derive from phone number
|
||||
logger.warn("User ID not provided in request. Using derived ID from phone number: {}", resolvedUserId);
|
||||
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", "");
|
||||
logger.warn("User ID not provided in query parameters. Using derived ID from phone number: {}", resolvedUserId);
|
||||
}
|
||||
if (request.queryInput() == null || request.queryInput().text() == null || request.queryInput().text().text() == null || request.queryInput().text().text().trim().isEmpty()) {
|
||||
|
||||
if (request.queryInput() == null || request.queryInput().text() == null ||
|
||||
request.queryInput().text().text() == null || request.queryInput().text().text().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Dialogflow query input text is required.");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package com.example.service;
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
package com.example.service.conversation;
|
||||
|
||||
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||
import com.example.exception.FirestorePersistenceException;
|
||||
import com.example.mapper.FirestoreConversationMapper;
|
||||
import com.example.mapper.conversation.FirestoreConversationMapper;
|
||||
import com.example.repository.FirestoreBaseRepository;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import com.google.cloud.firestore.DocumentSnapshot;
|
||||
@@ -17,6 +22,13 @@ import reactor.core.scheduler.Schedulers;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Service for managing 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 FirestoreConversationService {
|
||||
|
||||
@@ -31,7 +43,7 @@ public class FirestoreConversationService {
|
||||
}
|
||||
|
||||
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
||||
logger.info("Attempting to save conversation entry to Firestore for session {}. Type: {}", sessionId, newEntry.type().name());
|
||||
logger.info("Attempting to save conversation entry to Firestore for session {}. Entity: {}", sessionId, newEntry.entity().name());
|
||||
return Mono.fromRunnable(() -> {
|
||||
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
||||
WriteBatch batch = firestoreBaseRepository.createBatch();
|
||||
@@ -41,13 +53,13 @@ public class FirestoreConversationService {
|
||||
// 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 {}. Type: {}", userId, sessionId, newEntry.type().name());
|
||||
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 {}. Type: {}", userId, sessionId, newEntry.type().name());
|
||||
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);
|
||||
@@ -1,26 +1,34 @@
|
||||
package com.example.service;
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
package com.example.service.conversation;
|
||||
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 com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Service for managing conversation sessions using a memory store (Redis).
|
||||
* It caches and retrieves `ConversationSessionDTO` objects, maintaining a mapping
|
||||
* from a user's phone number to their active session ID. This service uses
|
||||
* a time-to-live (TTL) to manage session expiration and provides a fast
|
||||
* reactive interface for persisting new conversation entries and fetching sessions.
|
||||
*/
|
||||
@Service
|
||||
public class MemoryStoreConversationService {
|
||||
|
||||
public class MemoryStoreConversationService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MemoryStoreConversationService.class);
|
||||
private static final String SESSION_KEY_PREFIX = "conversation:session:";
|
||||
private static final String PHONE_TO_SESSION_KEY_PREFIX = "conversation: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 MemoryStoreConversationService(
|
||||
ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate,
|
||||
@@ -28,34 +36,29 @@ public class MemoryStoreConversationService {
|
||||
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 session {}. Type: {}", sessionId, newEntry.type().name());
|
||||
|
||||
logger.info("Attempting to save entry to Redis for 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 session {} with new entry type {} in Redis.", sessionId, newEntry.type().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)
|
||||
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, SESSION_TTL));
|
||||
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, SESSION_TTL))
|
||||
.then(); // <--- ADD THIS .then() WITHOUT ARGUMENTS
|
||||
})
|
||||
.doOnSuccess(success -> logger.info("Successfully saved updated session and phone mapping to Redis for session {}. Entry Type: {}", sessionId, newEntry.type().name()))
|
||||
.doOnError(e -> logger.error("Error appending entry to Redis for session {}: {}", sessionId, e.getMessage(), e))
|
||||
.then();
|
||||
.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));
|
||||
}
|
||||
|
||||
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
|
||||
if (telefono == null || telefono.trim().isEmpty()) {
|
||||
if (telefono == null || telefono.isBlank()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
String phoneToSessionKey = PHONE_TO_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);
|
||||
@@ -70,4 +73,4 @@ public class MemoryStoreConversationService {
|
||||
})
|
||||
.doOnError(e -> logger.error("Error retrieving session by phone number {}: {}", telefono, e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.example.service.notification;
|
||||
|
||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||
import com.example.exception.FirestorePersistenceException;
|
||||
import com.example.mapper.notification.FirestoreNotificationMapper;
|
||||
import com.example.repository.FirestoreBaseRepository;
|
||||
import com.google.cloud.Timestamp;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import com.google.cloud.firestore.FieldValue;
|
||||
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.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Service
|
||||
public class FirestoreNotificationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FirestoreNotificationService.class);
|
||||
private static final String NOTIFICATION_COLLECTION_PATH_FORMAT = "artifacts/%s/notifications";
|
||||
private static final String FIELD_MESSAGES = "notificaciones";
|
||||
private static final String FIELD_LAST_UPDATED = "ultimaActualizacion";
|
||||
private static final String FIELD_PHONE_NUMBER = "telefono";
|
||||
private static final String FIELD_NOTIFICATION_ID = "sessionId";
|
||||
|
||||
private final FirestoreBaseRepository firestoreBaseRepository;
|
||||
private final FirestoreNotificationMapper firestoreNotificationMapper;
|
||||
|
||||
public FirestoreNotificationService(
|
||||
FirestoreBaseRepository firestoreBaseRepository,
|
||||
FirestoreNotificationMapper firestoreNotificationMapper) {
|
||||
this.firestoreBaseRepository = firestoreBaseRepository;
|
||||
this.firestoreNotificationMapper = firestoreNotificationMapper;
|
||||
}
|
||||
|
||||
|
||||
public Mono<Void> saveOrAppendNotificationEntry(NotificationDTO newEntry) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
String phoneNumber = newEntry.telefono();
|
||||
if (phoneNumber == null || phoneNumber.isBlank()) {
|
||||
throw new IllegalArgumentException("Phone number is required to manage notification entries.");
|
||||
}
|
||||
|
||||
// Use the phone number as the document ID for the session.
|
||||
String notificationSessionId = phoneNumber;
|
||||
|
||||
DocumentReference notificationDocRef = getNotificationDocumentReference(notificationSessionId);
|
||||
Map<String, Object> entryMap = firestoreNotificationMapper.mapNotificationDTOToMap(newEntry);
|
||||
|
||||
try {
|
||||
// Check if the session document exists.
|
||||
boolean docExists = notificationDocRef.get().get().exists();
|
||||
|
||||
if (docExists) {
|
||||
// If the document exists, append the new entry to the 'notificaciones' array.
|
||||
Map<String, Object> updates = Map.of(
|
||||
FIELD_MESSAGES, FieldValue.arrayUnion(entryMap),
|
||||
FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now()))
|
||||
);
|
||||
notificationDocRef.update(updates).get();
|
||||
logger.info("Successfully appended new entry to notification session {} in Firestore.", notificationSessionId);
|
||||
} else {
|
||||
// If the document does not exist, create a new session document.
|
||||
Map<String, Object> newSessionData = Map.of(
|
||||
FIELD_NOTIFICATION_ID, notificationSessionId,
|
||||
FIELD_PHONE_NUMBER, phoneNumber,
|
||||
"fechaCreacion", Timestamp.of(java.util.Date.from(Instant.now())),
|
||||
FIELD_LAST_UPDATED, Timestamp.of(java.util.Date.from(Instant.now())),
|
||||
FIELD_MESSAGES, Collections.singletonList(entryMap)
|
||||
);
|
||||
notificationDocRef.set(newSessionData).get();
|
||||
logger.info("Successfully created a new notification session {} in Firestore.", notificationSessionId);
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
logger.error("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) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.error("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();
|
||||
}
|
||||
|
||||
private String getNotificationCollectionPath() {
|
||||
return String.format(NOTIFICATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
||||
}
|
||||
|
||||
private DocumentReference getNotificationDocumentReference(String notificationId) {
|
||||
String collectionPath = getNotificationCollectionPath();
|
||||
return firestoreBaseRepository.getDocumentReference(collectionPath, notificationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.example.service.notification;
|
||||
|
||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||
import com.example.dto.dialogflow.notification.NotificationSessionDTO;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.time.Instant;
|
||||
|
||||
@Service
|
||||
public class MemoryStoreNotificationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MemoryStoreNotificationService.class);
|
||||
private final ReactiveRedisTemplate<String, NotificationSessionDTO> notificationRedisTemplate;
|
||||
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
||||
private static final String NOTIFICATION_KEY_PREFIX = "notification:";
|
||||
private static final String PHONE_TO_SESSION_KEY_PREFIX = "notification:phone_to_notification:";
|
||||
private final Duration notificationTtl = Duration.ofHours(24);
|
||||
|
||||
public MemoryStoreNotificationService(
|
||||
ReactiveRedisTemplate<String, NotificationSessionDTO> notificationRedisTemplate,
|
||||
ReactiveRedisTemplate<String, String> stringRedisTemplate,
|
||||
ObjectMapper objectMapper) {
|
||||
this.notificationRedisTemplate = notificationRedisTemplate;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
|
||||
}
|
||||
|
||||
public Mono<Void> saveOrAppendNotificationEntry(NotificationDTO newEntry) {
|
||||
String phoneNumber = newEntry.telefono();
|
||||
if (phoneNumber == null || phoneNumber.isBlank()) {
|
||||
return Mono.error(new IllegalArgumentException("Phone number is required to manage notification entries."));
|
||||
}
|
||||
|
||||
//noote: Use the phone number as the session ID for notifications
|
||||
String notificationSessionId = phoneNumber;
|
||||
|
||||
return getCachedNotificationSession(notificationSessionId)
|
||||
.flatMap(existingSession -> {
|
||||
// Session exists, append the new entry
|
||||
List<NotificationDTO> updatedEntries = new ArrayList<>(existingSession.notificaciones());
|
||||
updatedEntries.add(newEntry);
|
||||
NotificationSessionDTO updatedSession = new NotificationSessionDTO(
|
||||
notificationSessionId,
|
||||
phoneNumber,
|
||||
existingSession.fechaCreacion(),
|
||||
Instant.now(),
|
||||
updatedEntries
|
||||
);
|
||||
return Mono.just(updatedSession);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
// No session found, create a new one
|
||||
NotificationSessionDTO newSession = new NotificationSessionDTO(
|
||||
notificationSessionId,
|
||||
phoneNumber,
|
||||
Instant.now(),
|
||||
Instant.now(),
|
||||
Collections.singletonList(newEntry)
|
||||
);
|
||||
return Mono.just(newSession);
|
||||
}))
|
||||
.flatMap(this::cacheNotificationSession)
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Boolean> cacheNotificationSession(NotificationSessionDTO session) {
|
||||
String key = NOTIFICATION_KEY_PREFIX + session.sessionId();
|
||||
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + session.telefono();
|
||||
|
||||
return notificationRedisTemplate.opsForValue().set(key, session, notificationTtl)
|
||||
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, session.sessionId(), notificationTtl));
|
||||
}
|
||||
|
||||
public Mono<NotificationSessionDTO> getCachedNotificationSession(String sessionId) {
|
||||
String key = NOTIFICATION_KEY_PREFIX + sessionId;
|
||||
return notificationRedisTemplate.opsForValue().get(key)
|
||||
.doOnSuccess(notification -> {
|
||||
if (notification != null) {
|
||||
logger.info("Notification session with ID {} retrieved from MemoryStore.", sessionId);
|
||||
} else {
|
||||
logger.debug("Notification session with ID {} not found in MemoryStore.", sessionId);
|
||||
}
|
||||
})
|
||||
.doOnError(e -> logger.error("Error retrieving notification session with ID {} from MemoryStore: {}", sessionId, e.getMessage(), e));
|
||||
}
|
||||
|
||||
public Mono<String> getNotificationIdForPhone(String phone) {
|
||||
String key = PHONE_TO_SESSION_KEY_PREFIX + phone;
|
||||
return stringRedisTemplate.opsForValue().get(key)
|
||||
.doOnSuccess(sessionId -> {
|
||||
if (sessionId != null) {
|
||||
logger.info("Session ID {} found for phone {}.", sessionId, phone);
|
||||
} else {
|
||||
logger.debug("Session ID not found for phone {}.", phone);
|
||||
}
|
||||
})
|
||||
.doOnError(e -> logger.error("Error retrieving session ID for phone {} from MemoryStore: {}", phone,
|
||||
e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.example.service.notification;
|
||||
|
||||
import com.example.dto.dialogflow.notification.ExternalNotRequestDTO;
|
||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||
import com.example.dto.dialogflow.conversation.QueryInputDTO;
|
||||
import com.example.dto.dialogflow.conversation.QueryParamsDTO;
|
||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||
import com.example.service.base.DialogflowClientService;
|
||||
import com.example.service.conversation.MemoryStoreConversationService;
|
||||
import com.example.util.SessionIdGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.example.dto.dialogflow.conversation.TextInputDTO;
|
||||
|
||||
@Service
|
||||
public class NotificationManagerService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotificationManagerService.class);
|
||||
private static final String NOTIFICATION_TEXT_PARAM = "notificationText";
|
||||
private static final String eventName = "notificacion";
|
||||
|
||||
|
||||
private final DialogflowClientService dialogflowClientService;
|
||||
private final FirestoreNotificationService firestoreNotificationService;
|
||||
private final MemoryStoreNotificationService memoryStoreNotificationService;
|
||||
private final MemoryStoreConversationService memoryStoreConversationService;
|
||||
|
||||
@Value("${dialogflow.default-language-code:es}")
|
||||
private String defaultLanguageCode;
|
||||
|
||||
public NotificationManagerService(
|
||||
DialogflowClientService dialogflowClientService,
|
||||
FirestoreNotificationService firestoreNotificationService,
|
||||
MemoryStoreNotificationService memoryStoreNotificationService,
|
||||
MemoryStoreConversationService memoryStoreConversationService) {
|
||||
this.dialogflowClientService = dialogflowClientService;
|
||||
this.firestoreNotificationService = firestoreNotificationService;
|
||||
this.memoryStoreNotificationService = memoryStoreNotificationService;
|
||||
this.memoryStoreConversationService = memoryStoreConversationService;
|
||||
}
|
||||
|
||||
public Mono<DetectIntentResponseDTO> processNotification(ExternalNotRequestDTO externalRequest) {
|
||||
Objects.requireNonNull(externalRequest, "ExternalNotRequestDTO cannot be null.");
|
||||
|
||||
String telefono = externalRequest.phoneNumber();
|
||||
if (telefono == null || telefono.isBlank()) {
|
||||
logger.warn("No phone number provided in ExternalNotRequestDTO. Cannot process notification.");
|
||||
return Mono.error(new IllegalArgumentException("Phone number is required."));
|
||||
}
|
||||
|
||||
// 1. Persist the incoming notification entry
|
||||
String newNotificationId = SessionIdGenerator.generateStandardSessionId();
|
||||
NotificationDTO newNotificationEntry = new NotificationDTO(newNotificationId,telefono, Instant.now(),
|
||||
externalRequest.text(),eventName, defaultLanguageCode,Collections.emptyMap()
|
||||
);
|
||||
Mono<Void> persistenceMono = memoryStoreNotificationService.saveOrAppendNotificationEntry(newNotificationEntry)
|
||||
.doOnSuccess(v -> {
|
||||
logger.info("Notification for phone {} cached. Kicking off async Firestore write-back.", telefono);
|
||||
firestoreNotificationService.saveOrAppendNotificationEntry(newNotificationEntry)
|
||||
.subscribe(
|
||||
ignored -> logger.debug("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
|
||||
Mono<ConversationSessionDTO> sessionMono = memoryStoreConversationService.getSessionByTelefono(telefono)
|
||||
.doOnNext(session -> logger.info("Found existing conversation session {} for phone number {}", session.sessionId(), telefono))
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
||||
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));
|
||||
}));
|
||||
|
||||
// 3. Send notification text to Dialogflow using the resolved conversation session
|
||||
return persistenceMono.then(sessionMono)
|
||||
.flatMap(session -> {
|
||||
final String sessionId = session.sessionId();
|
||||
logger.info("Sending notification text to Dialogflow using conversation session: {}", sessionId);
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("telefono", telefono);
|
||||
parameters.put(NOTIFICATION_TEXT_PARAM, newNotificationEntry.texto());
|
||||
|
||||
// Use a TextInputDTO to correctly build the QueryInputDTO
|
||||
TextInputDTO textInput = new TextInputDTO(newNotificationEntry.texto());
|
||||
QueryInputDTO queryInput = new QueryInputDTO(textInput, null, defaultLanguageCode);
|
||||
|
||||
DetectIntentRequestDTO detectIntentRequest = new DetectIntentRequestDTO(
|
||||
queryInput,
|
||||
new QueryParamsDTO(parameters)
|
||||
);
|
||||
|
||||
return dialogflowClientService.detectIntent(sessionId, detectIntentRequest);
|
||||
})
|
||||
.doOnSuccess(response -> logger.info("Finished processing notification. Dialogflow response received for phone {}.", telefono))
|
||||
.doOnError(e -> logger.error("Overall error in NotificationManagerService: {}", e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.example.service;
|
||||
/*
|
||||
* 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.summary;
|
||||
|
||||
import com.example.dto.gemini.ConversationSummaryRequest;
|
||||
import com.example.dto.gemini.ConversationSummaryResponse;
|
||||
import com.example.dto.gemini.ConversationSessionSummaryDTO;
|
||||
import com.example.dto.gemini.ConversationEntrySummaryDTO;
|
||||
import com.example.repository.FirestoreBaseRepository;
|
||||
import com.example.service.base.GeminiClientService;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -26,6 +32,8 @@ public class ConversationSummaryService {
|
||||
private static final String DEFAULT_GEMINI_MODEL_NAME = "gemini-2.0-flash-001";
|
||||
private static final Float DEFAULT_TEMPERATURE = 0.7f;
|
||||
private static final Integer DEFAULT_MAX_OUTPUT_TOKENS = 800;
|
||||
private static final Float DEFAULT_tOPP = 0.1f;
|
||||
|
||||
|
||||
public ConversationSummaryService(GeminiClientService geminiService, FirestoreBaseRepository firestoreBaseRepository) {
|
||||
this.geminiService = geminiService;
|
||||
@@ -55,6 +63,8 @@ public class ConversationSummaryService {
|
||||
? request.temperature() : DEFAULT_TEMPERATURE;
|
||||
Integer actualMaxOutputTokens = (request.maxOutputTokens() != null)
|
||||
? request.maxOutputTokens() : DEFAULT_MAX_OUTPUT_TOKENS;
|
||||
Float actualTopP = (request.top_P() != null)
|
||||
? request.top_P() : DEFAULT_tOPP;
|
||||
|
||||
String collectionPath = String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
||||
String documentId = sessionId;
|
||||
@@ -99,7 +109,8 @@ public class ConversationSummaryService {
|
||||
fullPromptForGemini,
|
||||
actualTemperature,
|
||||
actualMaxOutputTokens,
|
||||
actualModelName
|
||||
actualModelName,
|
||||
actualTopP
|
||||
);
|
||||
|
||||
if (summaryText == null || summaryText.trim().isEmpty()) {
|
||||
Reference in New Issue
Block a user