UPDATE 24-sept

This commit is contained in:
PAVEL PALMA
2025-09-24 17:20:48 -06:00
parent 26cbe71d2f
commit 535a1d8ca0
4 changed files with 113 additions and 43 deletions

58
pom.xml
View File

@@ -21,7 +21,7 @@
<spring-cloud-gcp.version>5.4.0</spring-cloud-gcp.version> <spring-cloud-gcp.version>5.4.0</spring-cloud-gcp.version>
<spring-cloud.version>2023.0.0</spring-cloud.version> <spring-cloud.version>2023.0.0</spring-cloud.version>
<lettuce.version>6.4.0.RELEASE</lettuce.version> <lettuce.version>6.4.0.RELEASE</lettuce.version>
<spring-framework.version>6.1.20</spring-framework.version> <spring-framework.version>6.1.21</spring-framework.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -61,36 +61,88 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId> <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.5.0</version> <version>2.5.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.cloud</groupId> <groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-data-firestore</artifactId> <artifactId>spring-cloud-gcp-starter-data-firestore</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.cloud</groupId> <groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-data-firestore</artifactId> <artifactId>spring-cloud-gcp-data-firestore</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.cloud</groupId> <groupId>com.google.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-storage</artifactId> <artifactId>spring-cloud-gcp-starter-storage</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.cloud</groupId> <groupId>com.google.cloud</groupId>
@@ -172,6 +224,10 @@
<version>2.10.0</version> <version>2.10.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>

View File

@@ -2,24 +2,28 @@
* 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 org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.io.IOException; import java.io.IOException;
import java.io.Reader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
import java.io.InputStream;
/** /**
* Service to classify message entries (user text input from DetectIntent) into predefined categories * Classifies a user's text input into a predefined category using a Gemini
* like "CONVERSATION" or "NOTIFICATION" using Gemini model. * 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 @Service
public class MessageEntryFilter { public class MessageEntryFilter {
@@ -46,27 +50,29 @@ public class MessageEntryFilter {
public static final String CATEGORY_NOTIFICATION = "NOTIFICATION"; public static final String CATEGORY_NOTIFICATION = "NOTIFICATION";
public static final String CATEGORY_UNKNOWN = "UNKNOWN"; public static final String CATEGORY_UNKNOWN = "UNKNOWN";
public static final String CATEGORY_ERROR = "ERROR"; public static final String CATEGORY_ERROR = "ERROR";
private String promptTemplate; private String promptTemplate;
public MessageEntryFilter(GeminiClientService geminiService) { public MessageEntryFilter(GeminiClientService geminiService) {
this.geminiService = Objects.requireNonNull(geminiService, "GeminiClientService cannot be null for MessageEntryFilter."); this.geminiService = Objects.requireNonNull(geminiService,
"GeminiClientService cannot be null for MessageEntryFilter.");
} }
@PostConstruct @PostConstruct
public void loadPromptTemplate() { public void loadPromptTemplate() {
try { try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(promptFilePath)) {
ClassPathResource resource = new ClassPathResource(promptFilePath); if (inputStream == null) {
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { throw new IOException("Resource not found: " + promptFilePath);
this.promptTemplate = FileCopyUtils.copyToString(reader);
} }
logger.info("Successfully loaded prompt template from '{}'.", promptFilePath); byte[] fileBytes = inputStream.readAllBytes();
this.promptTemplate = new String(fileBytes, StandardCharsets.UTF_8);
logger.info("Successfully loaded prompt template from '" + promptFilePath + "'.");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to load prompt template from '{}'. Please ensure the file exists.", promptFilePath, 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); throw new IllegalStateException("Could not load prompt template.", e);
} }
} }
public String classifyMessage(String queryInputText, String notificationsJson, String conversationJson) { public String classifyMessage(String queryInputText, String notificationsJson, String conversationJson) {
if (queryInputText == null || queryInputText.isBlank()) { if (queryInputText == null || queryInputText.isBlank()) {
logger.warn("Query input text for classification is null or blank. Returning {}.", CATEGORY_UNKNOWN); logger.warn("Query input text for classification is null or blank. Returning {}.", CATEGORY_UNKNOWN);

View File

@@ -8,16 +8,24 @@ import com.example.service.notification.MemoryStoreNotificationService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; 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 @Service
public class NotificationContextResolver { public class NotificationContextResolver {
@@ -43,21 +51,24 @@ public class NotificationContextResolver {
private String promptTemplate; private String promptTemplate;
public NotificationContextResolver(GeminiClientService geminiService, MemoryStoreNotificationService memoryStoreNotificationService) { public NotificationContextResolver(GeminiClientService geminiService,
MemoryStoreNotificationService memoryStoreNotificationService) {
this.geminiService = Objects.requireNonNull(geminiService, this.geminiService = Objects.requireNonNull(geminiService,
"GeminiClientService cannot be null for NotificationContextResolver."); "GeminiClientService cannot be null for NotificationContextResolver.");
} }
@PostConstruct @PostConstruct
public void loadPromptTemplate() { public void loadPromptTemplate() {
try { try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(promptFilePath)) {
ClassPathResource resource = new ClassPathResource(promptFilePath); if (inputStream == null) {
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { throw new IOException("Resource not found: " + promptFilePath);
this.promptTemplate = FileCopyUtils.copyToString(reader);
} }
logger.info("Successfully loaded prompt template from '{}'.", promptFilePath); byte[] fileBytes = inputStream.readAllBytes();
this.promptTemplate = new String(fileBytes, StandardCharsets.UTF_8);
logger.info("Successfully loaded prompt template from '" + promptFilePath + "'.");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to load prompt template from '{}'. Please ensure the file exists.", promptFilePath, 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); throw new IllegalStateException("Could not load prompt template.", e);
} }
} }

View File

@@ -12,35 +12,32 @@ import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.DocumentSnapshot; import com.google.cloud.firestore.DocumentSnapshot;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@Component
public class FirestoreDataImporter { public class FirestoreDataImporter {
private static final Logger logger = LoggerFactory.getLogger(FirestoreDataImporter.class); private static final Logger logger = LoggerFactory.getLogger(FirestoreDataImporter.class);
private static final String QUICK_REPLIES_COLLECTION_PATH_FORMAT = "artifacts/%s/quick-replies"; private static final String QUICK_REPLIES_COLLECTION_PATH_FORMAT = "artifacts/%s/quick-replies";
@Autowired private final FirestoreBaseRepository firestoreBaseRepository;
private FirestoreBaseRepository firestoreBaseRepository; private final ObjectMapper objectMapper;
@Autowired private final boolean isImporterEnabled;
private ObjectMapper objectMapper;
@Autowired public FirestoreDataImporter(FirestoreBaseRepository firestoreBaseRepository, ObjectMapper objectMapper) {
private Environment env; this.firestoreBaseRepository = firestoreBaseRepository;
this.objectMapper = objectMapper;
this.isImporterEnabled = Boolean.parseBoolean(System.getProperty("firestore.data.importer.enabled"));
@PostConstruct }
public void importDataOnStartup() {
if (Boolean.parseBoolean(env.getProperty("firestore.data.importer.enabled"))) { public void runImport() {
if (isImporterEnabled) {
try { try {
importQuickReplies(); importQuickReplies();
} catch (Exception e) { } catch (Exception e) {
@@ -91,4 +88,4 @@ public class FirestoreDataImporter {
} }
} }
} }
} }