UPDATE fix quick replies
This commit is contained in:
15
Dockerfile 2
Normal file
15
Dockerfile 2
Normal file
@@ -0,0 +1,15 @@
|
||||
# Java 21.0.6
|
||||
# 'jammy' refers to Ubuntu 22.04 LTS, which is a stable and widely used base.
|
||||
|
||||
# FROM maven:3.9.6-eclipse-temurin-21 AS builder
|
||||
# FROM quay.ocp.banorte.com/base/openjdk-21:maven_3.8 AS builder
|
||||
# WORKDIR /app
|
||||
# COPY pom.xml .
|
||||
# COPY src ./src
|
||||
# RUN mvn -B clean install -DskipTests -Dmaven.javadoc.skip=true
|
||||
# FROM eclipse-temurin:21.0.3_9-jre-jammy
|
||||
FROM quay.ocp.banorte.com/golden/openjdk-21:latest
|
||||
# COPY --from=builder /app/target/app-jovenes-service-orchestrator-0.0.1-SNAPSHOT.jar app.jar
|
||||
COPY target/app-jovenes-service-orchestrator-0.0.1-SNAPSHOT.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
236
README 2.md
Normal file
236
README 2.md
Normal file
@@ -0,0 +1,236 @@
|
||||
*Key Versions & Management:*
|
||||
|
||||
* *Java Version:* `21`
|
||||
* *Spring Boot Version:* `3.2.5` (defined in the parent POM)
|
||||
* *Spring Cloud GCP Version:* `5.3.0` (managed via `spring-cloud-gcp-dependencies`)
|
||||
* *Spring Cloud Version:* `2023.0.0` (managed via `spring-cloud-dependencies`)
|
||||
|
||||
|
||||
This project is a **Spring Boot Service Orchestrator** running on **Java 21**.
|
||||
|
||||
Here is step-by-step guide to getting this deployed locally in your IDE.
|
||||
|
||||
-----
|
||||
|
||||
### Step 1: Ensure Prerequisites
|
||||
|
||||
Before we touch the code, we need to make sure your local machine matches the project requirements found in the `pom.xml` and `Dockerfile`.
|
||||
|
||||
1. **Install Java 21 JDK:** The project explicitly requires Java 21.
|
||||
* *Check:* Run `java -version` in your terminal. If it doesn't say "21", you need to install it.
|
||||
2. **Install Maven:** This is used to build the project dependencies.
|
||||
3. **Install the "Extension Pack for Java" in VS Code:** This includes tools for Maven, debugging, and IntelliSense.
|
||||
4. **Install Docker (Desktop or Engine):** We will need this to run a local Redis instance.
|
||||
|
||||
-----
|
||||
|
||||
### Step 2: The "Redis Gotcha" (Local Infrastructure)
|
||||
|
||||
If you look at `src/main/resources/application-dev.properties`, you will see this line:
|
||||
`spring.data.redis.host=localhost`.
|
||||
|
||||
|
||||
1. **Start Redis in Docker:**
|
||||
Open your terminal and run:
|
||||
```bash
|
||||
docker run --name local-redis -p 6379:6379 -d redis
|
||||
```
|
||||
2. **Verify it's running:**
|
||||
Run `docker ps`. You should see redis running on port `6379`.
|
||||
|
||||
-----
|
||||
|
||||
### Step 3: Google Cloud Authentication
|
||||
|
||||
This application connects to **Firestore**, **Dialogflow CX**, and **Vertex AI (Gemini)**. It uses the "Application Default Credentials" strategy.
|
||||
|
||||
1. **Install the Google Cloud CLI (`gcloud`)** if you haven't already.
|
||||
2. **Login:**
|
||||
In your terminal, run:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
*This will open a browser window. Log in with your Google account that has access to the `app-jovenes` project.*
|
||||
|
||||
-----
|
||||
|
||||
### Step 4: Configure Local Properties
|
||||
|
||||
We need to tell the application to look at your *local* Redis instead of the cloud one.
|
||||
|
||||
1. Open `src/main/resources/application.properties`.
|
||||
|
||||
2. Ensure the active profile is set to `dev`:
|
||||
|
||||
```properties
|
||||
spring.profiles.active=dev
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### Step 5: Build the Project
|
||||
|
||||
Now let's download all the dependencies defined in the `pom.xml`.
|
||||
|
||||
1. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P).
|
||||
2. Type **"Maven: Execute Commands"** -\> select the project -\> **"install"**.
|
||||
* *Alternative:* Open the built-in terminal and run:
|
||||
```bash
|
||||
mvn clean install -DskipTests
|
||||
```
|
||||
* *Why skip tests?* The tests might try to connect to real cloud services or check specific configs that might fail on the first local run. Let's just get it compiling first.
|
||||
|
||||
-----
|
||||
|
||||
### Step 6: Run the Application
|
||||
|
||||
1. Navigate to `src/main/java/com/example/Orchestrator.java`.
|
||||
2. You should see a small "Run | Debug" button appear just above the `public static void main` line.
|
||||
3. Click **Run**.
|
||||
|
||||
**What to watch for in the Console:**
|
||||
|
||||
* You want to see the Spring Boot logo.
|
||||
* Look for `Started Orchestrator in X seconds`.
|
||||
* Look for `Netty started on port 8080` (since this is a WebFlux app).
|
||||
|
||||
-----
|
||||
|
||||
### Step 7: Verify it's working
|
||||
|
||||
Since this is an API, let's test the health or a simple endpoint.
|
||||
|
||||
1. The app runs on port **8080** (defined in Dockerfile).
|
||||
2. The API has Swagger documentation configured.
|
||||
3. Open your browser and go to:
|
||||
`http://localhost:8080/webjars/swagger-ui/index.html` .
|
||||
* *Note:* If Swagger isn't loading, check the console logs for the exact context path.
|
||||
|
||||
### Summary Checklist for you:
|
||||
|
||||
* [ ] Java 21 Installed?
|
||||
* [ ] Docker running Redis on localhost:6379?
|
||||
* [ ] `gcloud auth application-default login` run?
|
||||
* [ ] `application-dev.properties` updated to use `localhost` for Redis?
|
||||
|
||||
### Examples of endpoint call
|
||||
|
||||
### 1\. The Standard Conversation (Dialogflow)
|
||||
|
||||
This is the most common flow. It simulates a user sending a message like "Hola" to the bot. The orchestrator will route this to Dialogflow CX.
|
||||
|
||||
**Request:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/dialogflow/detect-intent \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"mensaje": "Hola, ¿quien eres?",
|
||||
"usuario": {
|
||||
"telefono": "5550001234",
|
||||
"nickname": "DiegoLocal"
|
||||
},
|
||||
"canal": "whatsapp",
|
||||
"tipo": "INICIO"
|
||||
}'
|
||||
```
|
||||
|
||||
**What to expect:**
|
||||
|
||||
* **Status:** `200 OK`
|
||||
* **Response:** A JSON object containing `responseText` (the answer from Dialogflow) and `responseId`.
|
||||
* **Logs:** Check your VS Code terminal. You should see logs like `Initiating detectIntent for session...`.
|
||||
|
||||
-----
|
||||
|
||||
### 2\. The "Smart" Notification Flow (Gemini Router)
|
||||
|
||||
This is the cool part. We will first "push" a notification to the user, and then simulate the user asking a question about it.
|
||||
|
||||
**Step A: Push the Notification**
|
||||
This tells the system: *"Hey, user 5550001234 just received this alert."*
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/dialogflow/notification \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"texto": "Tu tarjeta *1234 ha sido bloqueada por seguridad.",
|
||||
"telefono": "5550001234",
|
||||
"parametrosOcultos": {
|
||||
"motivo": "intento_fraude_detectado",
|
||||
"ubicacion": "CDMX",
|
||||
"fecha": "Hoy"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
* **Check Logs:** You should see `Notification for phone 5550001234 cached`.
|
||||
|
||||
**Step B: User asks a follow-up (The Test)**
|
||||
Now, ask a question that requires context from that notification.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/dialogflow/detect-intent \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"mensaje": "¿Por qué fue bloqueada?",
|
||||
"usuario": {
|
||||
"telefono": "5550001234"
|
||||
},
|
||||
"canal": "whatsapp",
|
||||
"tipo": "CONVERSACION"
|
||||
}'
|
||||
```
|
||||
|
||||
* **What happens internally:** The `MessageEntryFilter` (Gemini) will see the previous notification in the history and classify this as a `NOTIFICATION` follow-up, routing it to the LLM instead of standard Dialogflow.
|
||||
|
||||
-----
|
||||
|
||||
### 3\. Quick Replies (Static Content)
|
||||
|
||||
This tests the `QuickRepliesManagerService`. It fetches a JSON screen definition from your local files (e.g., `home.json`).
|
||||
|
||||
**Request:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/quick-replies/screen \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"usuario": {
|
||||
"telefono": "5550001234"
|
||||
},
|
||||
"canal": "app",
|
||||
"tipo": "INICIO",
|
||||
"pantallaContexto": "pagos"
|
||||
}'
|
||||
```
|
||||
|
||||
**What to expect:**
|
||||
|
||||
* **Response:** A JSON object with a `quick_replies` field containing the title "Home" (loaded from `home.json`).
|
||||
|
||||
-----
|
||||
|
||||
### 4\. Reset Everything (Purge)
|
||||
|
||||
If you want to start fresh (clear the cache and history for "Local"), run this:
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/v1/data-purge/all
|
||||
```
|
||||
|
||||
* **Logs:** You'll see `Starting Redis data purge` and `Starting Firestore data purge`.
|
||||
|
||||
### 5\. Optional testing the llm response with uuid
|
||||
|
||||
```bash
|
||||
/api/v1/llm/tune-response
|
||||
{
|
||||
"sessionInfo": {
|
||||
"parameters": {
|
||||
"uuid": "21270589-184e-4a1a-922d-fb48464211e8"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
241
pom 2.xml
Normal file
241
pom 2.xml
Normal file
@@ -0,0 +1,241 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.11</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>app-jovenes-service-orchestrator</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>app-jovenes-service-orchestrator</name>
|
||||
<description>This serivce handle conversations over Dialogflow and multiple Storage GCP services</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<spring-cloud-gcp.version>5.4.0</spring-cloud-gcp.version>
|
||||
<spring-cloud.version>2023.0.0</spring-cloud.version>
|
||||
<lettuce.version>6.4.0.RELEASE</lettuce.version>
|
||||
<spring-framework.version>6.1.21</spring-framework.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring-cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>spring-cloud-gcp-dependencies</artifactId>
|
||||
<version>${spring-cloud-gcp.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>libraries-bom</artifactId>
|
||||
<version>26.40.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-bom</artifactId>
|
||||
<version>2024.0.8</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<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>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
<version>2.5.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>spring-cloud-gcp-starter-data-firestore</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>spring-cloud-gcp-data-firestore</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>spring-cloud-gcp-starter-storage</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-dialogflow-cx</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.genai</groupId>
|
||||
<artifactId>google-genai</artifactId>
|
||||
<version>1.14.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java-util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-parameter-names</artifactId>
|
||||
<version>2.19.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.api</groupId>
|
||||
<artifactId>gax</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-dlp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-codec-http2</artifactId>
|
||||
<version>4.1.125.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-handler</artifactId>
|
||||
<version>4.1.125.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-common</artifactId>
|
||||
<version>4.1.125.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-codec-http</artifactId>
|
||||
<version>4.1.125.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-codec</artifactId>
|
||||
<version>4.1.125.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>3.25.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.minidev</groupId>
|
||||
<artifactId>json-smart</artifactId>
|
||||
<version>2.5.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xmlunit</groupId>
|
||||
<artifactId>xmlunit-core</artifactId>
|
||||
<version>2.10.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.18.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -63,6 +63,7 @@ public class ConversationManagerService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConversationManagerService.class);
|
||||
|
||||
private static final long SESSION_RESET_THRESHOLD_MINUTES = 30;
|
||||
private static final long SCREEN_CONTEXT_TIMEOUT_MINUTES = 10; // fix for the quick replies screen
|
||||
private static final String CONV_HISTORY_PARAM = "conversation_history";
|
||||
private final ExternalConvRequestMapper externalRequestToDialogflowMapper;
|
||||
private final DialogflowClientService dialogflowServiceClient;
|
||||
@@ -123,12 +124,25 @@ public class ConversationManagerService {
|
||||
externalrequest.pantallaContexto());
|
||||
return memoryStoreConversationService.getSessionByTelefono(externalrequest.user().telefono())
|
||||
.flatMap(session -> {
|
||||
if (session != null && session.pantallaContexto() != null
|
||||
&& !session.pantallaContexto().isBlank()) {
|
||||
logger.info(
|
||||
"Detected 'pantallaContexto' in session. Delegating to QuickRepliesManagerService.");
|
||||
return quickRepliesManagerService.manageConversation(obfuscatedRequest);
|
||||
}
|
||||
boolean isContextStale = false;
|
||||
if (session.lastModified() != null) {
|
||||
long minutesSinceLastUpdate = java.time.Duration.between(session.lastModified(), java.time.Instant.now()).toMinutes();
|
||||
if (minutesSinceLastUpdate > SCREEN_CONTEXT_TIMEOUT_MINUTES) {
|
||||
isContextStale = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (session != null && session.pantallaContexto() != null
|
||||
&& !session.pantallaContexto().isBlank()
|
||||
&& !isContextStale) {
|
||||
logger.info("Detected active 'pantallaContexto'. Delegating to QuickRepliesManagerService.");
|
||||
return quickRepliesManagerService.manageConversation(obfuscatedRequest);
|
||||
}
|
||||
|
||||
// Remove the old QR and continue as normal conversation.
|
||||
if (isContextStale && session.pantallaContexto() != null) {
|
||||
logger.info("Detected STALE 'pantallaContexto'. Ignoring and proceeding with normal flow.");
|
||||
}
|
||||
return continueManagingConversation(obfuscatedRequest);
|
||||
})
|
||||
.switchIfEmpty(continueManagingConversation(obfuscatedRequest));
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.stream.IntStream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.example.dto.dialogflow.conversation.QueryResultDTO;
|
||||
|
||||
import com.example.service.conversation.ConversationManagerService;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -109,16 +109,36 @@ public class QuickRepliesManagerService {
|
||||
userMessagesCount = 0;
|
||||
}
|
||||
|
||||
if (userMessagesCount == 0) { // Is the first user message in the Quick-Replies flow
|
||||
// This is the second message of the flow. Return the full list.
|
||||
return persistConversationTurn(session, userEntry)
|
||||
.then(quickReplyContentService.getQuickReplies(session.pantallaContexto()))
|
||||
.flatMap(quickReplyDTO -> {
|
||||
ConversationEntryDTO agentEntry = ConversationEntryDTO
|
||||
.forAgentWithMessage(quickReplyDTO.toString());
|
||||
return persistConversationTurn(session, agentEntry)
|
||||
.thenReturn(new DetectIntentResponseDTO(session.sessionId(), null, quickReplyDTO));
|
||||
});
|
||||
if (userMessagesCount == 0) {
|
||||
// El usuario acaba de responder al menú.
|
||||
// Validamos si lo que escribió coincide con alguna opción del menú.
|
||||
return quickReplyContentService.getQuickReplies(session.pantallaContexto())
|
||||
.flatMap(quickReplyDTO -> {
|
||||
// Verificamos si el texto del usuario coincide con algún título de las preguntas
|
||||
boolean esOpcionValida = quickReplyDTO.preguntas().stream()
|
||||
.anyMatch(p -> p.titulo().equalsIgnoreCase(externalRequest.message().trim()));
|
||||
|
||||
if (esOpcionValida) {
|
||||
// Si coincide, guardamos el mensaje y procesamos la respuesta (lógica existente para respuesta automática)
|
||||
String respuesta = quickReplyDTO.preguntas().stream()
|
||||
.filter(p -> p.titulo().equalsIgnoreCase(externalRequest.message().trim()))
|
||||
.findFirst().get().respuesta();
|
||||
|
||||
QueryResultDTO queryResult = new QueryResultDTO(respuesta, null);
|
||||
DetectIntentResponseDTO response = new DetectIntentResponseDTO(session.sessionId(), queryResult, null);
|
||||
|
||||
return persistConversationTurn(session, userEntry) // Guardamos el "input" del usuario
|
||||
.then(memoryStoreConversationService.updateSession(session.withPantallaContexto(null))) // Limpiamos contexto
|
||||
.then(persistConversationTurn(session, ConversationEntryDTO.forAgentWithMessage(respuesta))) // Guardamos la respuesta
|
||||
.thenReturn(response);
|
||||
|
||||
} else {
|
||||
// Si el usuario escribe algo que no es del menu valida y manda a Dialog
|
||||
logger.info("El input del usuario no coincide con las Quick Replies. Redirigiendo a Dialogflow normal.");
|
||||
return memoryStoreConversationService.updateSession(session.withPantallaContexto(null))
|
||||
.then(conversationManagerService.manageConversation(externalRequest));
|
||||
}
|
||||
});
|
||||
} else if (userMessagesCount == 1) { // Is the second user message in the QR flow
|
||||
// This is the third message of the flow. Filter and end.
|
||||
return persistConversationTurn(session, userEntry)
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.example.service.unit_testing;
|
||||
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.conversation.*;
|
||||
import com.example.dto.quickreplies.QuestionDTO;
|
||||
import com.example.dto.quickreplies.QuickReplyDTO;
|
||||
import com.example.mapper.conversation.ConversationEntryMapper;
|
||||
import com.example.service.conversation.ConversationManagerService;
|
||||
import com.example.service.conversation.FirestoreConversationService;
|
||||
import com.example.service.conversation.MemoryStoreConversationService;
|
||||
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||
import com.example.service.quickreplies.QuickReplyContentService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class QuickRepliesManagerServiceTest {
|
||||
|
||||
@Mock private MemoryStoreConversationService memoryStoreConversationService;
|
||||
@Mock private FirestoreConversationService firestoreConversationService;
|
||||
@Mock private QuickReplyContentService quickReplyContentService;
|
||||
@Mock private ConversationManagerService conversationManagerService;
|
||||
@Mock private ConversationEntryMapper conversationEntryMapper;
|
||||
|
||||
private QuickRepliesManagerService quickRepliesManagerService;
|
||||
|
||||
// Test Data
|
||||
private final String PHONE = "5555555555";
|
||||
private final String SESSION_ID = "session-123";
|
||||
private final String USER_ID = "user_by_phone_5555555555";
|
||||
private final String CONTEXTO = "pagos";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
quickRepliesManagerService = new QuickRepliesManagerService(
|
||||
conversationManagerService,
|
||||
memoryStoreConversationService,
|
||||
firestoreConversationService,
|
||||
quickReplyContentService,
|
||||
conversationEntryMapper
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("manageConversation - Count 0 - NO MATCH: Should clear context and delegate to Dialogflow")
|
||||
void manageConversation_Count0_NoMatch_ShouldDelegate() {
|
||||
// 1. SETUP: User typed "Hola", but context "pagos" is active.
|
||||
ExternalConvRequestDTO request = new ExternalConvRequestDTO("Hola", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null);
|
||||
ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO);
|
||||
|
||||
// Mock Session Retrieval
|
||||
when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session));
|
||||
|
||||
// Mock History: Only the SYSTEM message (The Menu) exists. Count = 0.
|
||||
ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp");
|
||||
when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg));
|
||||
|
||||
// Mock QR Content: The menu has options, but "Hola" is NOT one of them.
|
||||
QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo is 10");
|
||||
QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1));
|
||||
when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto));
|
||||
|
||||
// Mock Orchestrator Delegation (The expected outcome)
|
||||
DetectIntentResponseDTO delegatedResponse = new DetectIntentResponseDTO("df-response", new QueryResultDTO("Hola soy Beto", null));
|
||||
when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty()); // Clearing context
|
||||
when(conversationManagerService.manageConversation(request)).thenReturn(Mono.just(delegatedResponse));
|
||||
|
||||
// 2. EXECUTE
|
||||
StepVerifier.create(quickRepliesManagerService.manageConversation(request))
|
||||
.expectNext(delegatedResponse)
|
||||
.verifyComplete();
|
||||
|
||||
// 3. VERIFY
|
||||
// Ensure we cleared the context
|
||||
verify(memoryStoreConversationService).updateSession(argThat(s -> s.pantallaContexto() == null));
|
||||
// Ensure we called the normal conversation manager
|
||||
verify(conversationManagerService).manageConversation(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("manageConversation - Count 0 - MATCH: Should return QR Answer")
|
||||
void manageConversation_Count0_Match_ShouldAnswer() {
|
||||
// 1. SETUP: User typed "Ver Saldo" (Valid Option)
|
||||
ExternalConvRequestDTO request = new ExternalConvRequestDTO("Ver Saldo", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null);
|
||||
ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO);
|
||||
|
||||
when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session));
|
||||
|
||||
// Count 0 (Last message was System)
|
||||
ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp");
|
||||
when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg));
|
||||
|
||||
// Valid Option exists
|
||||
QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo es 100 pesos");
|
||||
QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1));
|
||||
when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto));
|
||||
|
||||
// Mocks for saving the conversation turn
|
||||
mockPersistence();
|
||||
when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty());
|
||||
|
||||
// 2. EXECUTE
|
||||
StepVerifier.create(quickRepliesManagerService.manageConversation(request))
|
||||
.assertNext(response -> {
|
||||
// Expect the pre-defined answer from the JSON
|
||||
assert response.queryResult().responseText().equals("Tu saldo es 100 pesos");
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
// Verify we did NOT delegate
|
||||
verify(conversationManagerService, never()).manageConversation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("manageConversation - Count 1 - NO MATCH: Should save User Msg, Clear, and Delegate")
|
||||
void manageConversation_Count1_NoMatch_ShouldDelegate() {
|
||||
// 1. SETUP: User typed "Gracias" (Not an option)
|
||||
ExternalConvRequestDTO request = new ExternalConvRequestDTO("Gracias", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null);
|
||||
ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO);
|
||||
|
||||
when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session));
|
||||
|
||||
// Mock History: System msg -> User msg (Invalid) -> Now this is the 2nd user msg (Count 1 logic)
|
||||
// Wait, logic says count is messages AFTER last init.
|
||||
// Sys (Init) -> User("Hola" - ignored previously) -> Current Request("Gracias")
|
||||
ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp");
|
||||
ConversationMessageDTO userMsg1 = new ConversationMessageDTO(MessageType.USER, Instant.now(), "Hola", null, "whatsapp");
|
||||
when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg, userMsg1));
|
||||
|
||||
QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo es 10");
|
||||
QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1));
|
||||
when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto));
|
||||
|
||||
// Mock persistence for the user message (Manual save in the else block)
|
||||
mockPersistence();
|
||||
|
||||
// Mock Delegation
|
||||
DetectIntentResponseDTO delegatedResponse = new DetectIntentResponseDTO("df-response", new QueryResultDTO("De nada", null));
|
||||
when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty());
|
||||
when(conversationManagerService.manageConversation(request)).thenReturn(Mono.just(delegatedResponse));
|
||||
|
||||
// 2. EXECUTE
|
||||
StepVerifier.create(quickRepliesManagerService.manageConversation(request))
|
||||
.expectNext(delegatedResponse)
|
||||
.verifyComplete();
|
||||
|
||||
// 3. VERIFY
|
||||
// Ensure manual save was called (verifying flow of `persistConversationTurn`)
|
||||
verify(memoryStoreConversationService, times(1)).saveMessage(eq(SESSION_ID), any());
|
||||
verify(memoryStoreConversationService).updateSession(argThat(s -> s.pantallaContexto() == null));
|
||||
verify(conversationManagerService).manageConversation(request);
|
||||
}
|
||||
|
||||
// Helper to mock the complex save chain
|
||||
private void mockPersistence() {
|
||||
when(conversationEntryMapper.toConversationMessageDTO(any())).thenReturn(new ConversationMessageDTO(MessageType.USER, Instant.now(), "text", null, "wa"));
|
||||
when(memoryStoreConversationService.saveSession(any())).thenReturn(Mono.empty());
|
||||
when(memoryStoreConversationService.saveMessage(anyString(), any())).thenReturn(Mono.empty());
|
||||
when(firestoreConversationService.saveSession(any())).thenReturn(Mono.empty());
|
||||
when(firestoreConversationService.saveMessage(anyString(), any())).thenReturn(Mono.empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user