.
This commit is contained in:
214
src.bak/main/java/com/example/service/base/DataPurgeService.java
Normal file
214
src.bak/main/java/com/example/service/base/DataPurgeService.java
Normal file
@@ -0,0 +1,214 @@
|
||||
|
||||
package com.example.service.base;
|
||||
|
||||
|
||||
|
||||
import com.example.repository.FirestoreBaseRepository;
|
||||
|
||||
import com.google.cloud.firestore.CollectionReference;
|
||||
|
||||
import com.google.cloud.firestore.Firestore;
|
||||
|
||||
import com.google.cloud.firestore.QueryDocumentSnapshot;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
|
||||
|
||||
@Service
|
||||
|
||||
public class DataPurgeService {
|
||||
|
||||
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DataPurgeService.class);
|
||||
|
||||
|
||||
|
||||
private final ReactiveRedisTemplate<String, ?> redisTemplate;
|
||||
|
||||
private final FirestoreBaseRepository firestoreBaseRepository;
|
||||
|
||||
|
||||
|
||||
private final Firestore firestore;
|
||||
|
||||
|
||||
|
||||
@Autowired
|
||||
|
||||
public DataPurgeService(
|
||||
|
||||
@Qualifier("reactiveRedisTemplate") ReactiveRedisTemplate<String, ?> redisTemplate,
|
||||
|
||||
FirestoreBaseRepository firestoreBaseRepository, Firestore firestore) {
|
||||
|
||||
this.redisTemplate = redisTemplate;
|
||||
|
||||
this.firestoreBaseRepository = firestoreBaseRepository;
|
||||
|
||||
this.firestore = firestore;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Mono<Void> purgeAllData() {
|
||||
|
||||
return purgeRedis()
|
||||
|
||||
.then(purgeFirestore());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Mono<Void> purgeRedis() {
|
||||
|
||||
logger.info("Starting Redis data purge.");
|
||||
|
||||
return redisTemplate.getConnectionFactory().getReactiveConnection().serverCommands().flushAll()
|
||||
|
||||
.doOnSuccess(v -> logger.info("Successfully purged all data from Redis."))
|
||||
|
||||
.doOnError(e -> logger.error("Error purging data from Redis.", e))
|
||||
|
||||
.then();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Mono<Void> purgeFirestore() {
|
||||
|
||||
logger.info("Starting Firestore data purge.");
|
||||
|
||||
return Mono.fromRunnable(() -> {
|
||||
|
||||
try {
|
||||
|
||||
String appId = firestoreBaseRepository.getAppId();
|
||||
|
||||
String conversationsCollectionPath = String.format("artifacts/%s/conversations", appId);
|
||||
|
||||
String notificationsCollectionPath = String.format("artifacts/%s/notifications", appId);
|
||||
|
||||
|
||||
|
||||
// Delete 'mensajes' sub-collections in 'conversations'
|
||||
|
||||
logger.info("Deleting 'mensajes' sub-collections from '{}'", conversationsCollectionPath);
|
||||
|
||||
try {
|
||||
|
||||
List<QueryDocumentSnapshot> conversationDocuments = firestore.collection(conversationsCollectionPath).get().get().getDocuments();
|
||||
|
||||
for (QueryDocumentSnapshot document : conversationDocuments) {
|
||||
|
||||
String messagesCollectionPath = document.getReference().getPath() + "/mensajes";
|
||||
|
||||
logger.info("Deleting sub-collection: {}", messagesCollectionPath);
|
||||
|
||||
firestoreBaseRepository.deleteCollection(messagesCollectionPath, 50);
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
if (e.getMessage().contains("NOT_FOUND")) {
|
||||
|
||||
logger.warn("Collection '{}' not found, skipping.", conversationsCollectionPath);
|
||||
|
||||
} else {
|
||||
|
||||
throw e;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Delete the 'conversations' collection
|
||||
|
||||
logger.info("Deleting collection: {}", conversationsCollectionPath);
|
||||
|
||||
try {
|
||||
|
||||
firestoreBaseRepository.deleteCollection(conversationsCollectionPath, 50);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
if (e.getMessage().contains("NOT_FOUND")) {
|
||||
|
||||
logger.warn("Collection '{}' not found, skipping.", conversationsCollectionPath);
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
throw e;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Delete the 'notifications' collection
|
||||
|
||||
logger.info("Deleting collection: {}", notificationsCollectionPath);
|
||||
|
||||
try {
|
||||
|
||||
firestoreBaseRepository.deleteCollection(notificationsCollectionPath, 50);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
if (e.getMessage().contains("NOT_FOUND")) {
|
||||
|
||||
logger.warn("Collection '{}' not found, skipping.", notificationsCollectionPath);
|
||||
|
||||
} else {
|
||||
|
||||
throw e;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
logger.info("Successfully purged Firestore collections.");
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
logger.error("Error purging Firestore collections.", e);
|
||||
|
||||
throw new RuntimeException("Failed to purge Firestore collections.", e);
|
||||
|
||||
}
|
||||
|
||||
}).subscribeOn(Schedulers.boundedElastic()).then();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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.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.QueryParameters;
|
||||
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;
|
||||
import reactor.util.retry.Retry;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DialogflowClientService.class);
|
||||
|
||||
private final String dialogflowCxProjectId;
|
||||
private final String dialogflowCxLocation;
|
||||
private final String dialogflowCxAgentId;
|
||||
|
||||
private final DialogflowRequestMapper dialogflowRequestMapper;
|
||||
private final DialogflowResponseMapper dialogflowResponseMapper;
|
||||
private SessionsClient sessionsClient;
|
||||
|
||||
public DialogflowClientService(
|
||||
|
||||
@org.springframework.beans.factory.annotation.Value("${dialogflow.cx.project-id}") String dialogflowCxProjectId,
|
||||
@org.springframework.beans.factory.annotation.Value("${dialogflow.cx.location}") String dialogflowCxLocation,
|
||||
@org.springframework.beans.factory.annotation.Value("${dialogflow.cx.agent-id}") String dialogflowCxAgentId,
|
||||
DialogflowRequestMapper dialogflowRequestMapper,
|
||||
DialogflowResponseMapper dialogflowResponseMapper)
|
||||
throws IOException {
|
||||
|
||||
this.dialogflowCxProjectId = dialogflowCxProjectId;
|
||||
this.dialogflowCxLocation = dialogflowCxLocation;
|
||||
this.dialogflowCxAgentId = dialogflowCxAgentId;
|
||||
this.dialogflowRequestMapper = dialogflowRequestMapper;
|
||||
this.dialogflowResponseMapper = dialogflowResponseMapper;
|
||||
|
||||
try {
|
||||
String regionalEndpoint = String.format("%s-dialogflow.googleapis.com:443", dialogflowCxLocation);
|
||||
SessionsSettings sessionsSettings = SessionsSettings.newBuilder()
|
||||
.setEndpoint(regionalEndpoint)
|
||||
.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 - Test Agent version: {}", dialogflowCxAgentId);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create Dialogflow CX SessionsClient: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void closeSessionsClient() {
|
||||
if (sessionsClient != null) {
|
||||
sessionsClient.close();
|
||||
logger.info("Dialogflow CX SessionsClient closed.");
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<DetectIntentResponseDTO> detectIntent(
|
||||
String sessionId,
|
||||
DetectIntentRequestDTO request) {
|
||||
|
||||
Objects.requireNonNull(sessionId, "Dialogflow session ID cannot be null.");
|
||||
Objects.requireNonNull(request, "Dialogflow request DTO cannot be null.");
|
||||
|
||||
logger.info("Initiating detectIntent for session: {}", sessionId);
|
||||
|
||||
DetectIntentRequest.Builder detectIntentRequestBuilder;
|
||||
try {
|
||||
detectIntentRequestBuilder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(request);
|
||||
logger.debug("Obtained partial DetectIntentRequest.Builder from mapper for session: {}", sessionId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error(" Failed to map DTO to partial Protobuf request for session {}: {}", sessionId, e.getMessage());
|
||||
return Mono.error(new IllegalArgumentException("Invalid Dialogflow request input: " + e.getMessage()));
|
||||
}
|
||||
|
||||
SessionName sessionName = SessionName.newBuilder()
|
||||
.setProject(dialogflowCxProjectId)
|
||||
.setLocation(dialogflowCxLocation)
|
||||
.setAgent(dialogflowCxAgentId)
|
||||
.setSession(sessionId)
|
||||
.build();
|
||||
detectIntentRequestBuilder.setSession(sessionName.toString());
|
||||
logger.debug("Set session path {} on the request builder for session: {}", sessionName.toString(), sessionId);
|
||||
|
||||
QueryParameters.Builder queryParamsBuilder;
|
||||
if (detectIntentRequestBuilder.hasQueryParams()) {
|
||||
queryParamsBuilder = detectIntentRequestBuilder.getQueryParams().toBuilder();
|
||||
} else {
|
||||
queryParamsBuilder = QueryParameters.newBuilder();
|
||||
}
|
||||
|
||||
detectIntentRequestBuilder.setQueryParams(queryParamsBuilder.build());
|
||||
|
||||
// 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);
|
||||
})
|
||||
|
||||
.retryWhen(reactor.util.retry.Retry.backoff(3, java.time.Duration.ofSeconds(1))
|
||||
.filter(throwable -> {
|
||||
if (throwable instanceof ApiException apiException) {
|
||||
com.google.api.gax.rpc.StatusCode.Code code = apiException.getStatusCode().getCode();
|
||||
boolean isRetryable = code == com.google.api.gax.rpc.StatusCode.Code.INTERNAL ||
|
||||
code == com.google.api.gax.rpc.StatusCode.Code.UNAVAILABLE;
|
||||
if (isRetryable) {
|
||||
logger.warn("Retrying Dialogflow CX call for session {} due to transient error: {}", sessionId, code);
|
||||
}
|
||||
return isRetryable;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.doBeforeRetry(retrySignal -> logger.debug("Retry attempt #{} for session {}",
|
||||
retrySignal.totalRetries() + 1, sessionId))
|
||||
.onRetryExhaustedThrow((retrySpec, retrySignal) -> {
|
||||
logger.error("Dialogflow CX retries exhausted for session {}", sessionId);
|
||||
return retrySignal.failure();
|
||||
})
|
||||
)
|
||||
.onErrorMap(ApiException.class, e -> {
|
||||
String statusCode = e.getStatusCode().getCode().name();
|
||||
String message = e.getMessage();
|
||||
String detailedLog = message;
|
||||
|
||||
if (e.getCause() instanceof io.grpc.StatusRuntimeException grpcEx) {
|
||||
detailedLog = String.format("Status: %s, Message: %s, Trailers: %s",
|
||||
grpcEx.getStatus().getCode(),
|
||||
grpcEx.getStatus().getDescription(),
|
||||
grpcEx.getTrailers());
|
||||
}
|
||||
|
||||
logger.error("Dialogflow CX API error for session {}: details={}",
|
||||
sessionId, detailedLog, e);
|
||||
|
||||
return new DialogflowClientException(
|
||||
"Dialogflow CX API error: " + statusCode + " - " + message, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.stereotype.Service;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Classifies a user's text input into a predefined category using a Gemini
|
||||
* model.
|
||||
* It analyzes the user's query in the context of a conversation history and any
|
||||
* relevant notifications to determine if the message is part of the ongoing
|
||||
* dialogue
|
||||
* or an interruption. The classification is used to route the request to the
|
||||
* appropriate handler (e.g., a standard conversational flow or a specific
|
||||
* notification processor).
|
||||
*/
|
||||
@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 (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(promptFilePath)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Resource not found: " + promptFilePath);
|
||||
}
|
||||
byte[] fileBytes = inputStream.readAllBytes();
|
||||
this.promptTemplate = new String(fileBytes, StandardCharsets.UTF_8);
|
||||
logger.info("Successfully loaded prompt template from '" + promptFilePath + "'.");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load prompt template from '" + promptFilePath
|
||||
+ "'. Please ensure the file exists. Error: " + e.getMessage());
|
||||
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,
|
||||
conversationHistory,
|
||||
interruptingNotification,
|
||||
queryInputText
|
||||
);
|
||||
|
||||
logger.debug("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.strip().toUpperCase() : "") {
|
||||
case CATEGORY_CONVERSATION -> {
|
||||
logger.info("Classified as {}.", CATEGORY_CONVERSATION);
|
||||
yield CATEGORY_CONVERSATION;
|
||||
}
|
||||
case CATEGORY_NOTIFICATION -> {
|
||||
logger.info("Classified as {}.", CATEGORY_NOTIFICATION);
|
||||
yield CATEGORY_NOTIFICATION;
|
||||
}
|
||||
default -> {
|
||||
logger.warn("Gemini returned an unrecognised classification or was null/blank: '{}'. Expected '{}' or '{}'. Returning {}.",
|
||||
geminiResponse, CATEGORY_CONVERSATION, CATEGORY_NOTIFICATION, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.service.notification.MemoryStoreNotificationService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Resolves the conversational context of a user query by leveraging a large
|
||||
* language model (LLM). This service evaluates a user's question in the context
|
||||
* of a specific notification and conversation history, then decides if the
|
||||
* query
|
||||
* can be answered by the LLM or if it should be handled by a standard
|
||||
* Dialogflow agent.
|
||||
* The class loads an LLM prompt from an external file and dynamically
|
||||
* formats it with a user's query and other context to drive its decision-making
|
||||
* process.
|
||||
*/
|
||||
@Service
|
||||
public class NotificationContextResolver {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotificationContextResolver.class);
|
||||
private final GeminiClientService geminiService;
|
||||
|
||||
@Value("${notificationcontext.geminimodel:gemini-2.0-flash-001}")
|
||||
private String geminiModelNameResolver;
|
||||
|
||||
@Value("${notificationcontext.temperature:0.1f}")
|
||||
private Float resolverTemperature;
|
||||
|
||||
@Value("${notificationcontext.maxOutputTokens:1024}")
|
||||
private Integer resolverMaxOutputTokens;
|
||||
|
||||
@Value("${notificationcontext.topP:0.1f}")
|
||||
private Float resolverTopP;
|
||||
|
||||
@Value("${notificationcontext.prompt:prompts/notification_context_resolver.txt}")
|
||||
private String promptFilePath;
|
||||
|
||||
public static final String CATEGORY_DIALOGFLOW = "DIALOGFLOW";
|
||||
|
||||
private String promptTemplate;
|
||||
|
||||
public NotificationContextResolver(GeminiClientService geminiService,
|
||||
MemoryStoreNotificationService memoryStoreNotificationService) {
|
||||
this.geminiService = Objects.requireNonNull(geminiService,
|
||||
"GeminiClientService cannot be null for NotificationContextResolver.");
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void loadPromptTemplate() {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(promptFilePath)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Resource not found: " + promptFilePath);
|
||||
}
|
||||
byte[] fileBytes = inputStream.readAllBytes();
|
||||
this.promptTemplate = new String(fileBytes, StandardCharsets.UTF_8);
|
||||
logger.info("Successfully loaded prompt template from '" + promptFilePath + "'.");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load prompt template from '" + promptFilePath
|
||||
+ "'. Please ensure the file exists. Error: " + e.getMessage());
|
||||
throw new IllegalStateException("Could not load prompt template.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveContext(String queryInputText, String notificationsJson, String conversationJson,
|
||||
String metadata, String userId, String sessionId, String userPhoneNumber) {
|
||||
logger.debug("resolveContext -> queryInputText: {}, notificationsJson: {}, conversationJson: {}, metadata: {}",
|
||||
queryInputText, notificationsJson, conversationJson, metadata);
|
||||
if (queryInputText == null || queryInputText.isBlank()) {
|
||||
logger.warn("Query input text for context resolution is null or blank.", CATEGORY_DIALOGFLOW);
|
||||
return CATEGORY_DIALOGFLOW;
|
||||
}
|
||||
|
||||
String notificationContent = (notificationsJson != null && !notificationsJson.isBlank()) ? notificationsJson
|
||||
: "No metadata in notification.";
|
||||
|
||||
String conversationHistory = (conversationJson != null && !conversationJson.isBlank()) ? conversationJson
|
||||
: "No conversation history.";
|
||||
|
||||
String contextPrompt = String.format(
|
||||
this.promptTemplate,
|
||||
conversationHistory,
|
||||
notificationContent,
|
||||
metadata,
|
||||
queryInputText);
|
||||
|
||||
logger.debug("Sending context resolution request to Gemini for input (first 100 chars): '{}'...",
|
||||
queryInputText.substring(0, Math.min(queryInputText.length(), 100)));
|
||||
|
||||
try {
|
||||
String geminiResponse = geminiService.generateContent(
|
||||
contextPrompt,
|
||||
resolverTemperature,
|
||||
resolverMaxOutputTokens,
|
||||
geminiModelNameResolver,
|
||||
resolverTopP);
|
||||
|
||||
if (geminiResponse != null && !geminiResponse.isBlank()) {
|
||||
if (geminiResponse.trim().equalsIgnoreCase(CATEGORY_DIALOGFLOW)) {
|
||||
logger.debug("Resolved to {}. Input: '{}'", CATEGORY_DIALOGFLOW, queryInputText);
|
||||
return CATEGORY_DIALOGFLOW;
|
||||
} else {
|
||||
logger.debug("Resolved to a specific response. Input: '{}'", queryInputText);
|
||||
return geminiResponse;
|
||||
}
|
||||
} else {
|
||||
logger.warn("Gemini returned a null or blank response",
|
||||
queryInputText, CATEGORY_DIALOGFLOW);
|
||||
return CATEGORY_DIALOGFLOW;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("An error occurred during Gemini content generation for context resolution.", e);
|
||||
return CATEGORY_DIALOGFLOW;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user