Update 25-sept
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.llm.webhook.WebhookRequestDTO;
|
||||||
|
import com.example.dto.llm.webhook.SessionInfoDTO;
|
||||||
|
import com.example.dto.llm.webhook.WebhookResponseDTO;
|
||||||
|
import com.example.service.llm.LlmResponseTunerService;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
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/llm")
|
||||||
|
public class LlmResponseTunerController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LlmResponseTunerController.class);
|
||||||
|
|
||||||
|
private final LlmResponseTunerService llmResponseTunerService;
|
||||||
|
|
||||||
|
public LlmResponseTunerController(LlmResponseTunerService llmResponseTunerService) {
|
||||||
|
this.llmResponseTunerService = llmResponseTunerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tune-response")
|
||||||
|
public Mono<WebhookResponseDTO> tuneResponse(@RequestBody WebhookRequestDTO request) {
|
||||||
|
String uuid = (String) request.getSessionInfo().getParameters().get("uuid");
|
||||||
|
return llmResponseTunerService
|
||||||
|
.getValue(uuid)
|
||||||
|
.map(
|
||||||
|
value -> {
|
||||||
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
|
parameters.put("webhook_success", true);
|
||||||
|
parameters.put("response", value);
|
||||||
|
SessionInfoDTO sessionInfo = new SessionInfoDTO(parameters);
|
||||||
|
return new WebhookResponseDTO(sessionInfo);
|
||||||
|
})
|
||||||
|
.defaultIfEmpty(createErrorResponse("No response found for the given UUID.", false))
|
||||||
|
.onErrorResume(
|
||||||
|
e -> {
|
||||||
|
logger.error("Error tuning response: {}", e.getMessage());
|
||||||
|
return Mono.just(
|
||||||
|
createErrorResponse("An internal error occurred.", true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebhookResponseDTO createErrorResponse(String errorMessage, boolean isError) {
|
||||||
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
|
parameters.put("webhook_success", false);
|
||||||
|
parameters.put("error_message", errorMessage);
|
||||||
|
SessionInfoDTO sessionInfo = new SessionInfoDTO(parameters);
|
||||||
|
return new WebhookResponseDTO(sessionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleException(Exception e) {
|
||||||
|
logger.error("An unexpected error occurred: {}", e.getMessage());
|
||||||
|
Map<String, String> response = new HashMap<>();
|
||||||
|
response.put("error", "Internal Server Error");
|
||||||
|
response.put("message", "An unexpected error occurred. Please try again later.");
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(
|
||||||
|
IllegalArgumentException e) {
|
||||||
|
logger.error("Bad request: {}", e.getMessage());
|
||||||
|
Map<String, String> response = new HashMap<>();
|
||||||
|
response.put("error", "Bad Request");
|
||||||
|
response.put("message", e.getMessage());
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.example.dto.llm.webhook;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SessionInfoDTO {
|
||||||
|
|
||||||
|
private Map<String, Object> parameters;
|
||||||
|
|
||||||
|
public SessionInfoDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionInfoDTO(Map<String, Object> parameters) {
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getParameters() {
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParameters(Map<String, Object> parameters) {
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.dto.llm.webhook;
|
||||||
|
|
||||||
|
public class WebhookRequestDTO {
|
||||||
|
|
||||||
|
private SessionInfoDTO sessionInfo;
|
||||||
|
|
||||||
|
public WebhookRequestDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionInfoDTO getSessionInfo() {
|
||||||
|
return sessionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionInfo(SessionInfoDTO sessionInfo) {
|
||||||
|
this.sessionInfo = sessionInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.example.dto.llm.webhook;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class WebhookResponseDTO {
|
||||||
|
|
||||||
|
@JsonProperty("sessionInfo")
|
||||||
|
private SessionInfoDTO sessionInfo;
|
||||||
|
|
||||||
|
public WebhookResponseDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebhookResponseDTO(SessionInfoDTO sessionInfo) {
|
||||||
|
this.sessionInfo = sessionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionInfoDTO getSessionInfo() {
|
||||||
|
return sessionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionInfo(SessionInfoDTO sessionInfo) {
|
||||||
|
this.sessionInfo = sessionInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.example.exception;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
@ExceptionHandler(DialogflowClientException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleDialogflowClientException(
|
||||||
|
DialogflowClientException ex) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("error", "Error communicating with Dialogflow");
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
logger.error("DialogflowClientException: {}", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(GeminiClientException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleGeminiClientException(GeminiClientException ex) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("error", "Error communicating with Gemini");
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
logger.error("GeminiClientException: {}", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleAllExceptions(Exception ex) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("error", "Internal Server Error");
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
logger.error("An unexpected error occurred: {}", ex.getMessage(), ex);
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ package com.example.mapper.messagefilter;
|
|||||||
|
|
||||||
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||||
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -18,8 +19,11 @@ import java.util.stream.Collectors;
|
|||||||
@Component
|
@Component
|
||||||
public class ConversationContextMapper {
|
public class ConversationContextMapper {
|
||||||
|
|
||||||
private static final int MESSAGE_LIMIT = 60;
|
@Value("${conversation.context.message.limit:60}")
|
||||||
private static final int DAYS_LIMIT = 30;
|
private int messageLimit;
|
||||||
|
|
||||||
|
@Value("${conversation.context.days.limit:30}")
|
||||||
|
private int daysLimit;
|
||||||
|
|
||||||
public String toText(ConversationSessionDTO session) {
|
public String toText(ConversationSessionDTO session) {
|
||||||
if (session == null || session.entries() == null || session.entries().isEmpty()) {
|
if (session == null || session.entries() == null || session.entries().isEmpty()) {
|
||||||
@@ -36,12 +40,12 @@ public class ConversationContextMapper {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Instant thirtyDaysAgo = Instant.now().minus(DAYS_LIMIT, ChronoUnit.DAYS);
|
Instant thirtyDaysAgo = Instant.now().minus(daysLimit, ChronoUnit.DAYS);
|
||||||
|
|
||||||
List<ConversationEntryDTO> recentEntries = session.entries().stream()
|
List<ConversationEntryDTO> recentEntries = session.entries().stream()
|
||||||
.filter(entry -> entry.timestamp().isAfter(thirtyDaysAgo))
|
.filter(entry -> entry.timestamp().isAfter(thirtyDaysAgo))
|
||||||
.sorted(Comparator.comparing(ConversationEntryDTO::timestamp).reversed())
|
.sorted(Comparator.comparing(ConversationEntryDTO::timestamp).reversed())
|
||||||
.limit(MESSAGE_LIMIT)
|
.limit(messageLimit)
|
||||||
.sorted(Comparator.comparing(ConversationEntryDTO::timestamp))
|
.sorted(Comparator.comparing(ConversationEntryDTO::timestamp))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,6 @@ public class MessageEntryFilter {
|
|||||||
|
|
||||||
String classificationPrompt = String.format(
|
String classificationPrompt = String.format(
|
||||||
this.promptTemplate,
|
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,
|
conversationHistory,
|
||||||
interruptingNotification,
|
interruptingNotification,
|
||||||
queryInputText
|
queryInputText
|
||||||
@@ -107,7 +104,7 @@ public class MessageEntryFilter {
|
|||||||
classifierTopP
|
classifierTopP
|
||||||
);
|
);
|
||||||
|
|
||||||
String resultCategory = switch (geminiResponse != null ? geminiResponse.trim().toUpperCase() : "") {
|
String resultCategory = switch (geminiResponse != null ? geminiResponse.strip().toUpperCase() : "") {
|
||||||
case CATEGORY_CONVERSATION -> {
|
case CATEGORY_CONVERSATION -> {
|
||||||
logger.info("Classified as {}.", CATEGORY_CONVERSATION);
|
logger.info("Classified as {}.", CATEGORY_CONVERSATION);
|
||||||
yield CATEGORY_CONVERSATION;
|
yield CATEGORY_CONVERSATION;
|
||||||
|
|||||||
@@ -2,6 +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.base;
|
package com.example.service.base;
|
||||||
|
|
||||||
import com.example.service.notification.MemoryStoreNotificationService;
|
import com.example.service.notification.MemoryStoreNotificationService;
|
||||||
@@ -75,9 +76,10 @@ public class NotificationContextResolver {
|
|||||||
|
|
||||||
public String resolveContext(String queryInputText, String notificationsJson, String conversationJson,
|
public String resolveContext(String queryInputText, String notificationsJson, String conversationJson,
|
||||||
String metadata, String userId, String sessionId, String userPhoneNumber) {
|
String metadata, String userId, String sessionId, String userPhoneNumber) {
|
||||||
logger.debug("resolveContext -> queryInputText: {}, notificationsJson: {}, conversationJson: {}, metadata: {}", queryInputText, notificationsJson, conversationJson, metadata);
|
logger.debug("resolveContext -> queryInputText: {}, notificationsJson: {}, conversationJson: {}, metadata: {}",
|
||||||
|
queryInputText, notificationsJson, conversationJson, metadata);
|
||||||
if (queryInputText == null || queryInputText.isBlank()) {
|
if (queryInputText == null || queryInputText.isBlank()) {
|
||||||
logger.warn("Query input text for context resolution is null or blank. Returning {}.", CATEGORY_DIALOGFLOW);
|
logger.warn("Query input text for context resolution is null or blank.", CATEGORY_DIALOGFLOW);
|
||||||
return CATEGORY_DIALOGFLOW;
|
return CATEGORY_DIALOGFLOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +109,15 @@ public class NotificationContextResolver {
|
|||||||
|
|
||||||
if (geminiResponse != null && !geminiResponse.isBlank()) {
|
if (geminiResponse != null && !geminiResponse.isBlank()) {
|
||||||
if (geminiResponse.trim().equalsIgnoreCase(CATEGORY_DIALOGFLOW)) {
|
if (geminiResponse.trim().equalsIgnoreCase(CATEGORY_DIALOGFLOW)) {
|
||||||
logger.info("Resolved to {}.", CATEGORY_DIALOGFLOW);
|
logger.debug("Resolved to {}. Input: '{}'", CATEGORY_DIALOGFLOW, queryInputText);
|
||||||
return CATEGORY_DIALOGFLOW;
|
return CATEGORY_DIALOGFLOW;
|
||||||
} else {
|
} else {
|
||||||
logger.info("Resolved to a specific response.");
|
logger.debug("Resolved to a specific response. Input: '{}'", queryInputText);
|
||||||
return geminiResponse;
|
return geminiResponse;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Gemini returned a null or blank response. Returning {}.",
|
logger.warn("Gemini returned a null or blank response",
|
||||||
CATEGORY_DIALOGFLOW);
|
queryInputText, CATEGORY_DIALOGFLOW);
|
||||||
return CATEGORY_DIALOGFLOW;
|
return CATEGORY_DIALOGFLOW;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -2,6 +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.conversation;
|
package com.example.service.conversation;
|
||||||
|
|
||||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||||
@@ -10,7 +11,8 @@ 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.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.conversation.QueryResultDTO;
|
import com.example.dto.dialogflow.conversation.QueryInputDTO;
|
||||||
|
import com.example.dto.dialogflow.notification.EventInputDTO;
|
||||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||||
import com.example.mapper.conversation.ExternalConvRequestMapper;
|
import com.example.mapper.conversation.ExternalConvRequestMapper;
|
||||||
import com.example.mapper.messagefilter.ConversationContextMapper;
|
import com.example.mapper.messagefilter.ConversationContextMapper;
|
||||||
@@ -20,6 +22,7 @@ import com.example.service.base.MessageEntryFilter;
|
|||||||
import com.example.service.base.NotificationContextResolver;
|
import com.example.service.base.NotificationContextResolver;
|
||||||
import com.example.service.notification.MemoryStoreNotificationService;
|
import com.example.service.notification.MemoryStoreNotificationService;
|
||||||
import com.example.service.quickreplies.QuickRepliesManagerService;
|
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||||
|
import com.example.service.llm.LlmResponseTunerService;
|
||||||
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;
|
||||||
@@ -32,8 +35,22 @@ import java.util.Collections;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orchestrates the full lifecycle of a user conversation,(this is the core of
|
||||||
|
* the entire integration layer service), acting as the central point for all
|
||||||
|
* inbound requests.
|
||||||
|
* This service manages conversational state by integrating both an in-memory
|
||||||
|
* cache (for active sessions)
|
||||||
|
* and a durable database (for conversation history). It intelligently routes
|
||||||
|
* incoming messages
|
||||||
|
* to the appropriate handler, which can include a standard Dialogflow agent, a
|
||||||
|
* notification-specific flow, or a direct LLM-based response. The class also
|
||||||
|
* ensures data integrity and security by applying Data Loss Prevention (DLP)
|
||||||
|
* to all incoming user messages before they are processed.
|
||||||
|
*/
|
||||||
@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);
|
||||||
@@ -53,6 +70,7 @@ public class ConversationManagerService {
|
|||||||
private final String dlpTemplateCompleteFlow;
|
private final String dlpTemplateCompleteFlow;
|
||||||
|
|
||||||
private final NotificationContextResolver notificationContextResolver;
|
private final NotificationContextResolver notificationContextResolver;
|
||||||
|
private final LlmResponseTunerService llmResponseTunerService;
|
||||||
|
|
||||||
public ConversationManagerService(
|
public ConversationManagerService(
|
||||||
DialogflowClientService dialogflowServiceClient,
|
DialogflowClientService dialogflowServiceClient,
|
||||||
@@ -66,6 +84,7 @@ public class ConversationManagerService {
|
|||||||
ConversationContextMapper conversationContextMapper,
|
ConversationContextMapper conversationContextMapper,
|
||||||
DataLossPrevention dataLossPrevention,
|
DataLossPrevention dataLossPrevention,
|
||||||
NotificationContextResolver notificationContextResolver,
|
NotificationContextResolver notificationContextResolver,
|
||||||
|
LlmResponseTunerService llmResponseTunerService,
|
||||||
@Value("${google.cloud.dlp.dlpTemplateCompleteFlow}") String dlpTemplateCompleteFlow) {
|
@Value("${google.cloud.dlp.dlpTemplateCompleteFlow}") String dlpTemplateCompleteFlow) {
|
||||||
this.dialogflowServiceClient = dialogflowServiceClient;
|
this.dialogflowServiceClient = dialogflowServiceClient;
|
||||||
this.firestoreConversationService = firestoreConversationService;
|
this.firestoreConversationService = firestoreConversationService;
|
||||||
@@ -79,6 +98,7 @@ public class ConversationManagerService {
|
|||||||
this.dataLossPrevention = dataLossPrevention;
|
this.dataLossPrevention = dataLossPrevention;
|
||||||
this.dlpTemplateCompleteFlow = dlpTemplateCompleteFlow;
|
this.dlpTemplateCompleteFlow = dlpTemplateCompleteFlow;
|
||||||
this.notificationContextResolver = notificationContextResolver;
|
this.notificationContextResolver = notificationContextResolver;
|
||||||
|
this.llmResponseTunerService = llmResponseTunerService;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +113,6 @@ public class ConversationManagerService {
|
|||||||
externalrequest.pantallaContexto());
|
externalrequest.pantallaContexto());
|
||||||
return memoryStoreConversationService.getSessionByTelefono(externalrequest.user().telefono())
|
return memoryStoreConversationService.getSessionByTelefono(externalrequest.user().telefono())
|
||||||
.flatMap(session -> {
|
.flatMap(session -> {
|
||||||
|
|
||||||
if (session != null && session.pantallaContexto() != null
|
if (session != null && session.pantallaContexto() != null
|
||||||
&& !session.pantallaContexto().isBlank()) {
|
&& !session.pantallaContexto().isBlank()) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -117,14 +136,21 @@ public class ConversationManagerService {
|
|||||||
"Failed to process external request due to mapping error: " + e.getMessage(), e));
|
"Failed to process external request due to mapping error: " + e.getMessage(), e));
|
||||||
}
|
}
|
||||||
|
|
||||||
final ConversationContext context;
|
Map<String, Object> params = Optional.ofNullable(request.queryParams())
|
||||||
try {
|
.map(queryParamsDTO -> queryParamsDTO.parameters())
|
||||||
context = resolveAndValidateRequest(request);
|
.orElse(Collections.emptyMap());
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
logger.error("Validation error for incoming request: {}", e.getMessage());
|
Object telefonoObj = params.get("telefono");
|
||||||
return Mono.error(e);
|
if (!(telefonoObj instanceof String) || ((String) telefonoObj).isBlank()) {
|
||||||
|
logger.error("Critical error: parameter is missing, not a String, or blank after mapping.");
|
||||||
|
return Mono.error(new IllegalStateException("Internal error: parameter is invalid."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String primaryPhoneNumber = (String) telefonoObj;
|
||||||
|
String resolvedUserId = params.get("usuario_id") instanceof String ? (String) params.get("usuario_id") : null;
|
||||||
|
String userMessageText = request.queryInput().text().text();
|
||||||
|
final ConversationContext context = new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
||||||
|
|
||||||
return handleMessageClassification(context, request);
|
return handleMessageClassification(context, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +197,7 @@ public class ConversationManagerService {
|
|||||||
.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
|
.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Primary Check (MemoryStore): Looking up session for phone number");
|
logger.info("Primary Check (MemoryStore): Looking up session for phone number: {}", userPhoneNumber);
|
||||||
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||||
.flatMap(session -> handleMessageClassification(context, request, session))
|
.flatMap(session -> handleMessageClassification(context, request, session))
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
@@ -274,7 +300,7 @@ public class ConversationManagerService {
|
|||||||
finalSessionId, error.getMessage(), error))));
|
finalSessionId, error.getMessage(), error))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<DetectIntentResponseDTO> startNotificationConversation(ConversationContext context,
|
public Mono<DetectIntentResponseDTO> startNotificationConversation(ConversationContext context,
|
||||||
DetectIntentRequestDTO request, NotificationDTO notification) {
|
DetectIntentRequestDTO request, NotificationDTO notification) {
|
||||||
final String userId = context.userId();
|
final String userId = context.userId();
|
||||||
final String userMessageText = context.userMessageText();
|
final String userMessageText = context.userMessageText();
|
||||||
@@ -283,8 +309,8 @@ public class ConversationManagerService {
|
|||||||
return memoryStoreNotificationService.getSessionByTelefono(userPhoneNumber)
|
return memoryStoreNotificationService.getSessionByTelefono(userPhoneNumber)
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
String newSessionId = SessionIdGenerator.generateStandardSessionId();
|
||||||
logger.info("No existing notification session found for phone number. Creating new session: {}",
|
logger.info("No existing notification session found for phone number {}. Creating new session: {}",
|
||||||
newSessionId);
|
userPhoneNumber, newSessionId);
|
||||||
return Mono.just(ConversationSessionDTO.create(newSessionId, userId, userPhoneNumber));
|
return Mono.just(ConversationSessionDTO.create(newSessionId, userId, userPhoneNumber));
|
||||||
}))
|
}))
|
||||||
.flatMap(session -> {
|
.flatMap(session -> {
|
||||||
@@ -301,6 +327,9 @@ public class ConversationManagerService {
|
|||||||
userPhoneNumber);
|
userPhoneNumber);
|
||||||
|
|
||||||
if (!NotificationContextResolver.CATEGORY_DIALOGFLOW.equals(resolvedContext)) {
|
if (!NotificationContextResolver.CATEGORY_DIALOGFLOW.equals(resolvedContext)) {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
llmResponseTunerService.setValue(uuid, resolvedContext).subscribe();
|
||||||
|
|
||||||
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText,
|
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText,
|
||||||
notification.parametros());
|
notification.parametros());
|
||||||
ConversationEntryDTO llmEntry = ConversationEntryDTO.forLlmConversation(resolvedContext,
|
ConversationEntryDTO llmEntry = ConversationEntryDTO.forLlmConversation(resolvedContext,
|
||||||
@@ -308,10 +337,21 @@ public class ConversationManagerService {
|
|||||||
|
|
||||||
return persistNotificationTurn(userId, sessionId, userEntry, userPhoneNumber)
|
return persistNotificationTurn(userId, sessionId, userEntry, userPhoneNumber)
|
||||||
.then(persistNotificationTurn(userId, sessionId, llmEntry, userPhoneNumber))
|
.then(persistNotificationTurn(userId, sessionId, llmEntry, userPhoneNumber))
|
||||||
.then(Mono.fromCallable(() -> {
|
.then(Mono.defer(() -> {
|
||||||
QueryResultDTO queryResult = new QueryResultDTO(resolvedContext,
|
EventInputDTO eventInput = new EventInputDTO("LLM_RESPONSE_PROCESSED");
|
||||||
java.util.Collections.emptyMap());
|
QueryInputDTO queryInput = new QueryInputDTO(null, eventInput,
|
||||||
return new DetectIntentResponseDTO(null, queryResult);
|
request.queryInput().languageCode());
|
||||||
|
DetectIntentRequestDTO newRequest = new DetectIntentRequestDTO(queryInput,
|
||||||
|
request.queryParams())
|
||||||
|
.withParameter("llm_reponse_uuid", uuid);
|
||||||
|
return dialogflowServiceClient.detectIntent(sessionId, newRequest)
|
||||||
|
.flatMap(response -> {
|
||||||
|
ConversationEntryDTO agentEntry = ConversationEntryDTO
|
||||||
|
.forAgent(response.queryResult());
|
||||||
|
return persistNotificationTurn(userId, sessionId, agentEntry,
|
||||||
|
userPhoneNumber)
|
||||||
|
.thenReturn(response);
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,39 +412,4 @@ public class ConversationManagerService {
|
|||||||
entry.type().name(), e.getMessage(), e));
|
entry.type().name(), e.getMessage(), e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
|
|
||||||
Map<String, Object> params = Optional.ofNullable(request.queryParams())
|
|
||||||
.map(queryParamsDTO -> queryParamsDTO.parameters())
|
|
||||||
.orElse(Collections.emptyMap());
|
|
||||||
String primaryPhoneNumber = null;
|
|
||||||
Object telefonoObj = params.get("telefono"); // Get from map
|
|
||||||
if (telefonoObj instanceof String) {
|
|
||||||
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 query parameters for conversation management.");
|
|
||||||
}
|
|
||||||
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 query_params is not a String (type: {}). Expected String.",
|
|
||||||
userIdObj.getClass().getName());
|
|
||||||
}
|
|
||||||
if (resolvedUserId == null || resolvedUserId.trim().isEmpty()) {
|
|
||||||
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", "");
|
|
||||||
logger.warn("User ID not provided in query parameters. Using derived ID from phone number");
|
|
||||||
}
|
|
||||||
if (request.queryInput() == null || request.queryInput().text() == null ||
|
|
||||||
request.queryInput().text().text() == null || request.queryInput().text().text().trim().isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("Dialogflow query input text is required.");
|
|
||||||
}
|
|
||||||
String userMessageText = request.queryInput().text().text();
|
|
||||||
return new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,14 @@ import com.example.util.TextObfuscator;
|
|||||||
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Implements a data loss prevention service by integrating with the
|
||||||
|
Google Cloud Data Loss Prevention (DLP) API. This service is responsible for
|
||||||
|
scanning a given text input to identify and obfuscate sensitive information based on
|
||||||
|
a specified DLP template. If the DLP API detects sensitive findings, the
|
||||||
|
original text is obfuscated to protect user data; otherwise, the original
|
||||||
|
text is returned.
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class DataLossPreventionImpl implements DataLossPrevention {
|
public class DataLossPreventionImpl implements DataLossPrevention {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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.llm;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface LlmResponseTunerService {
|
||||||
|
Mono<String> getValue(String key);
|
||||||
|
Mono<Void> setValue(String key, String value);
|
||||||
|
}
|
||||||
@@ -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.service.llm;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LlmResponseTunerServiceImpl implements LlmResponseTunerService {
|
||||||
|
|
||||||
|
private final ReactiveRedisTemplate<String, String> reactiveStringRedisTemplate;
|
||||||
|
private final String llmPreResponseCollectionName = "llm-pre-response:";
|
||||||
|
private final Duration ttl = Duration.ofHours(1);
|
||||||
|
|
||||||
|
public LlmResponseTunerServiceImpl(ReactiveRedisTemplate<String, String> reactiveStringRedisTemplate) {
|
||||||
|
this.reactiveStringRedisTemplate = reactiveStringRedisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<String> getValue(String key) {
|
||||||
|
return reactiveStringRedisTemplate.opsForValue().get(llmPreResponseCollectionName + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> setValue(String key, String value) {
|
||||||
|
return reactiveStringRedisTemplate.opsForValue().set(llmPreResponseCollectionName + key, value, ttl).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ public class QuickRepliesManagerService {
|
|||||||
|
|
||||||
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||||
.switchIfEmpty(Mono.error(
|
.switchIfEmpty(Mono.error(
|
||||||
new IllegalStateException("No quick reply session found for phone number" )))
|
new IllegalStateException("No quick reply session found for phone number")))
|
||||||
.flatMap(session -> {
|
.flatMap(session -> {
|
||||||
String userId = session.userId();
|
String userId = session.userId();
|
||||||
String sessionId = session.sessionId();
|
String sessionId = session.sessionId();
|
||||||
|
|||||||
@@ -71,3 +71,8 @@ firestore.data.importer.enabled=false
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
logging.level.root=INFO
|
logging.level.root=INFO
|
||||||
logging.level.com.example=INFO
|
logging.level.com.example=INFO
|
||||||
|
# =========================================================
|
||||||
|
# ConversationContext Configuration
|
||||||
|
# =========================================================
|
||||||
|
conversation.context.message.limit=${CONVERSATION_CONTEXT_MESSAGE_LIMIT}
|
||||||
|
conversation.context.days.limit=${CONVERSATION_CONTEXT_DAYS_LIMIT}
|
||||||
@@ -1,54 +1,93 @@
|
|||||||
You are an expert AI classification agent.
|
Hay un sistema de conversaciones entre un agente y un usuario. Durante
|
||||||
Your task is to analyze a user's final message after an external notification interrupts an ongoing conversation.
|
la conversación, una notificación puede entrar a la conversación de forma
|
||||||
|
abrupta, de tal forma que la siguiente interacción del usuario después
|
||||||
|
de la notificación puede corresponder a la conversación que estaba
|
||||||
|
sucediendo o puede ser un seguimiento a la notificación.
|
||||||
|
|
||||||
You will receive three pieces of information:
|
Tu tarea es identificar si la siguiente interacción del usuario es un
|
||||||
1. `CONVERSATION_HISTORY`: The dialogue between the agent and the user *before* the notification.
|
seguimiento a la notificación o una continuación de la conversación.
|
||||||
2. `INTERRUPTING_NOTIFICATION`: The specific alert that appeared.
|
|
||||||
3. `USER_FINAL_INPUT`: The user's message that you must classify.
|
|
||||||
|
|
||||||
Your goal is to determine if the `USER_FINAL_INPUT` is a reaction to the `INTERRUPTING_NOTIFICATION` or a continuation of the `CONVERSATION_HISTORY`.
|
Recibirás esta información:
|
||||||
|
|
||||||
**Classification Rules:**
|
- HISTORIAL_CONVERSACION: El diálogo entre el agente y el usuario antes
|
||||||
|
de la notificación.
|
||||||
|
- INTERRUPCION_NOTIFICACION: La notificación. Esta puede o no traer parámetros
|
||||||
|
los cuales refieren a detalles específicos de la notificación. Por ejemplo:
|
||||||
|
{ "vigencia": “12 de septiembre de 2025”, "credito_tipo" : "platinum" }
|
||||||
|
- INTERACCION_USUARIO: La siguiente interacción del usuario después de
|
||||||
|
la notificación.
|
||||||
|
|
||||||
* **`%s`**: Classify as `%s` if the `USER_FINAL_INPUT` is a direct question, comment, or reaction related to the `INTERRUPTING_NOTIFICATION`. The user has switched their focus to the notification.
|
Reglas:
|
||||||
* **`%s`**: Classify as `%s` if the `USER_FINAL_INPUT` ignores the notification and continues the topic from the `CONVERSATION_HISTORY`.
|
- Solo debes responder una palabra: NOTIFICATION o CONVERSATION. No agregues
|
||||||
* **Ambiguity Rule**: If the input is ambiguous (e.g., "ok, thanks"), default to `%s`. It's safer to assume the user is concluding the original topic.
|
o inventes otra palabra.
|
||||||
* **Acknowledgement Rule**: If the user briefly acknowledges the notification but immediately pivots back to the original conversation (e.g., "Okay thank you, but back to my question about loans..."), classify it as `%s`. The PRIMARY INTENT is to continue the original dialogue.
|
- Clasifica como NOTIFICATION si la siguiente interacción del usuario
|
||||||
|
es una clara respuesta o seguimiento a la notificación.
|
||||||
|
- Clasifica como CONVERSATION si la siguiente interacción del usuario
|
||||||
|
es un claro seguimiento al histórico de la conversación.
|
||||||
|
- Si la siguiente interacción del usuario es ambigua, clasifica
|
||||||
|
como CONVERSATION.
|
||||||
|
|
||||||
Your response must be a single word: `%s` or `%s`. Do not add any other text, punctuation, or explanations.
|
Ejemplos:
|
||||||
|
|
||||||
---
|
Ejemplo 1:
|
||||||
**Examples (Few-Shot Learning):**
|
HISTORIAL_CONVERSACION:
|
||||||
|
Agente: Claro, para un crédito de vehículo, las tasas actuales inician en el 1.2%% mensual.
|
||||||
|
Usuario: Entiendo, ¿y el plazo máximo de cuánto sería?
|
||||||
|
INTERRUPCION_NOTIFICACION:
|
||||||
|
Tu pago de la tarjeta de crédito por $1,500.00 ha sido procesado.
|
||||||
|
INTERACCION_USUARIO:
|
||||||
|
perfecto, cuando es la fecha de corte?
|
||||||
|
Clasificación: NOTIFICACION
|
||||||
|
|
||||||
**Example 1:**
|
Ejemplo 2:
|
||||||
`CONVERSATION_HISTORY`:
|
HISTORIAL_CONVERSACION:
|
||||||
Agent: Claro, para un crédito de vehículo, las tasas actuales inician en el 1.2%% mensual.
|
Agente: No es necesario, puedes completar todo el proceso para abrir tu cuenta desde nuestra app.
|
||||||
User: Entiendo, ¿y el plazo máximo de cuánto sería?
|
Usuario: Ok
|
||||||
`INTERRUPTING_NOTIFICATION`: Tu pago de la tarjeta de crédito por $1,500.00 ha sido procesado.
|
Agente: ¿Necesitas algo más?
|
||||||
`USER_FINAL_INPUT`: ¡Perfecto! Justo de eso quería saber, ¿ese pago ya se ve reflejado en el cupo disponible?
|
INTERRUPCION_NOTIFICACION:
|
||||||
Classification: %s
|
Tu estado de cuenta de Julio ya está disponible.
|
||||||
|
Parametros: {"fecha_corte": "30 de Agosto del 2025", "tipo_cuenta": "credito"}
|
||||||
|
INTERACCION_USUARIO:
|
||||||
|
que documentos necesito?
|
||||||
|
Clasificación: CONVERSACION
|
||||||
|
|
||||||
**Example 2:**
|
Ejemplo 3:
|
||||||
`CONVERSATION_HISTORY`:
|
HISTORIAL_CONVERSACION:
|
||||||
Agent: No es necesario, puedes completar todo el proceso para abrir tu cuenta desde nuestra app.
|
Agente: Ese fondo de inversión tiene un perfil de alto riesgo, pero históricamente ha dado un rendimiento superior al 15%% anual.
|
||||||
User: Ok, suena fácil.
|
Usuario: ok, entiendo
|
||||||
`INTERRUPTING_NOTIFICATION`: Tu estado de cuenta de Julio ya está disponible.
|
INTERRUPCION_NOTIFICACION:
|
||||||
`USER_FINAL_INPUT`: Bueno, y qué documentos necesito tener a la mano para hacerlo en la app?
|
Alerta: Tu cuenta de ahorros tiene un saldo bajo de $50.00.
|
||||||
Classification: %s
|
Parametros: {"fecha_retiro": "5 de septiembre del 2025", "tipo_cuenta": "ahorros"}
|
||||||
|
INTERACCION_USUARIO:
|
||||||
|
cuando fue el ultimo retiro?
|
||||||
|
Clasificación: NOTIFICACION
|
||||||
|
|
||||||
**Example 3:**
|
Ejemplo 4:
|
||||||
`CONVERSATION_HISTORY`:
|
HISTORIAL_CONVERSACION:
|
||||||
Agent: Ese fondo de inversión tiene un perfil de alto riesgo, pero históricamente ha dado un rendimiento superior al 15%% anual.
|
Usuario: Que es el CAT?
|
||||||
User: Suena interesante...
|
Agente: El CAT (Costo Anual Total) es un indicador financiero, expresado en un porcentaje anual, que refleja el costo total de un crédito, incluyendo no solo la tasa de interés, sino también todas las comisiones, gastos y otros cobros que genera.
|
||||||
`INTERRUPTING_NOTIFICATION`: Alerta: Tu cuenta de ahorros tiene un saldo bajo de $50.00.
|
INTERRUPCION_NOTIFICACION:
|
||||||
`USER_FINAL_INPUT`: Umm, ok gracias cuando fue el ultimo retiro?. Pero volviendo al fondo, ¿cuál es la inversión mínima para entrar?
|
Alerta: Se realizó un retiro en efectivo por $100.
|
||||||
Classification: %s
|
INTERACCION_USUARIO:
|
||||||
---
|
y este se aplica solo si dejo de pagar?
|
||||||
|
Clasificación: CONVERSACION
|
||||||
|
|
||||||
**Task:**
|
Ejemplo 5:
|
||||||
|
HISTORIAL_CONVERSACION:
|
||||||
|
Usuario: Cual es la tasa de hipoteca que manejan?
|
||||||
|
Agente: La tasa de una hipoteca depende tanto de factores económicos generales (inflación, tasas de referencia del banco central) como de factores individuales del solicitante (historial crediticio, monto del pago inicial, ingresos, endeudamiento, etc.)
|
||||||
|
INTERRUPCION_NOTIFICACION:
|
||||||
|
Hola, [Alias]: Pasó algo con la captura de tu INE y no se completó tu solicitud de tarjeta de crédito con folio 3421.
|
||||||
|
Parametros: {“solicitud_tarjeta_credito_vigencia”: “12 de septiembre de 2025”, “solicitud_tarjeta_credito_error”: “Error con el formato de la captura”, “solicitud_tarjeta_credito_tipo” : “platinum” }
|
||||||
|
INTERACCION_USUARIO:
|
||||||
|
cual fue el error?
|
||||||
|
Clasificación: NOTIFICACION
|
||||||
|
|
||||||
`CONVERSATION_HISTORY`:
|
Tarea:
|
||||||
%s
|
HISTORIAL_CONVERSACION:
|
||||||
`INTERRUPTING_NOTIFICATION`: %s
|
%s
|
||||||
`USER_FINAL_INPUT`: %s
|
INTERRUPCION_NOTIFICACION:
|
||||||
Classification:
|
%s
|
||||||
|
INTERACCION_USUARIO:
|
||||||
|
%s
|
||||||
|
Clasificación:
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
||||||
|
* Your use of it is subject to your agreement with Google.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.example.service.conversation;
|
||||||
|
|
||||||
|
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||||
|
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationContext;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationEntryDTO;
|
||||||
|
import com.example.dto.dialogflow.conversation.ConversationSessionDTO;
|
||||||
|
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.base.NotificationContextResolver;
|
||||||
|
import com.example.service.llm.LlmResponseTunerService;
|
||||||
|
import com.example.service.notification.MemoryStoreNotificationService;
|
||||||
|
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class ConversationManagerServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalConvRequestMapper externalRequestToDialogflowMapper;
|
||||||
|
@Mock
|
||||||
|
private DialogflowClientService dialogflowServiceClient;
|
||||||
|
@Mock
|
||||||
|
private FirestoreConversationService firestoreConversationService;
|
||||||
|
@Mock
|
||||||
|
private MemoryStoreConversationService memoryStoreConversationService;
|
||||||
|
@Mock
|
||||||
|
private QuickRepliesManagerService quickRepliesManagerService;
|
||||||
|
@Mock
|
||||||
|
private MessageEntryFilter messageEntryFilter;
|
||||||
|
@Mock
|
||||||
|
private MemoryStoreNotificationService memoryStoreNotificationService;
|
||||||
|
@Mock
|
||||||
|
private NotificationContextMapper notificationContextMapper;
|
||||||
|
@Mock
|
||||||
|
private ConversationContextMapper conversationContextMapper;
|
||||||
|
@Mock
|
||||||
|
private DataLossPrevention dataLossPrevention;
|
||||||
|
@Mock
|
||||||
|
private NotificationContextResolver notificationContextResolver;
|
||||||
|
@Mock
|
||||||
|
private LlmResponseTunerService llmResponseTunerService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ConversationManagerService conversationManagerService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startNotificationConversation_shouldSaveResolvedContextAndReturnIt() {
|
||||||
|
// Given
|
||||||
|
String userId = "test-user";
|
||||||
|
String userPhoneNumber = "1234567890";
|
||||||
|
String userMessageText = "test message";
|
||||||
|
String sessionId = "test-session";
|
||||||
|
String resolvedContext = "resolved context";
|
||||||
|
|
||||||
|
ConversationContext context = new ConversationContext(userId, null, userMessageText, userPhoneNumber);
|
||||||
|
DetectIntentRequestDTO request = new DetectIntentRequestDTO(null, null);
|
||||||
|
NotificationDTO notification = new NotificationDTO("1", "1234567890", Instant.now(), "test text", "test_event", "es", Collections.emptyMap(), "active");
|
||||||
|
ConversationSessionDTO session = ConversationSessionDTO.create(sessionId, userId, userPhoneNumber);
|
||||||
|
|
||||||
|
when(memoryStoreNotificationService.getSessionByTelefono(userPhoneNumber)).thenReturn(Mono.just(session));
|
||||||
|
when(conversationContextMapper.toTextWithLimits(session)).thenReturn("history");
|
||||||
|
when(notificationContextMapper.toText(notification)).thenReturn("notification text");
|
||||||
|
when(notificationContextResolver.resolveContext(anyString(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
|
||||||
|
.thenReturn(resolvedContext);
|
||||||
|
when(llmResponseTunerService.setValue(anyString(), anyString())).thenReturn(Mono.empty());
|
||||||
|
when(memoryStoreNotificationService.saveEntry(anyString(), anyString(), any(ConversationEntryDTO.class), anyString())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
// When
|
||||||
|
Mono<DetectIntentResponseDTO> result = conversationManagerService.startNotificationConversation(context, request, notification);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(response -> response.queryResult().responseText().equals(resolvedContext))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.service.llm;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ReactiveValueOperations;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class LlmResponseTunerServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReactiveRedisTemplate<String, String> reactiveStringRedisTemplate;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReactiveValueOperations<String, String> reactiveValueOperations;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private LlmResponseTunerServiceImpl llmResponseTunerService;
|
||||||
|
|
||||||
|
private final String llmPreResponseCollectionName = "llm-pre-response:";
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(reactiveStringRedisTemplate.opsForValue()).thenReturn(reactiveValueOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getValue_shouldReturnValueFromRedis() {
|
||||||
|
String key = "test_key";
|
||||||
|
String expectedValue = "test_value";
|
||||||
|
|
||||||
|
when(reactiveValueOperations.get(llmPreResponseCollectionName + key)).thenReturn(Mono.just(expectedValue));
|
||||||
|
|
||||||
|
StepVerifier.create(llmResponseTunerService.getValue(key))
|
||||||
|
.expectNext(expectedValue)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setValue_shouldSetValueInRedis() {
|
||||||
|
String key = "test_key";
|
||||||
|
String value = "test_value";
|
||||||
|
|
||||||
|
when(reactiveValueOperations.set(llmPreResponseCollectionName + key, value)).thenReturn(Mono.just(true));
|
||||||
|
|
||||||
|
StepVerifier.create(llmResponseTunerService.setValue(key, value))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user