UPGRADE 16-12
This commit is contained in:
230
README.md
230
README.md
@@ -4,3 +4,233 @@
|
|||||||
* *Spring Boot Version:* `3.2.5` (defined in the parent POM)
|
* *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 GCP Version:* `5.3.0` (managed via `spring-cloud-gcp-dependencies`)
|
||||||
* *Spring Cloud Version:* `2023.0.0` (managed via `spring-cloud-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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
4
pom.xml
4
pom.xml
@@ -166,10 +166,6 @@
|
|||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.module</groupId>
|
|
||||||
<artifactId>jackson-module-parameter-names</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.module</groupId>
|
<groupId>com.fasterxml.jackson.module</groupId>
|
||||||
<artifactId>jackson-module-parameter-names</artifactId>
|
<artifactId>jackson-module-parameter-names</artifactId>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package com.example.controller;
|
package com.example.controller;
|
||||||
|
|
||||||
|
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||||
import com.example.dto.quickreplies.QuickReplyScreenRequestDTO;
|
import com.example.dto.quickreplies.QuickReplyScreenRequestDTO;
|
||||||
import com.example.service.quickreplies.QuickRepliesManagerService;
|
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -29,9 +30,8 @@ public class QuickRepliesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/screen")
|
@PostMapping("/screen")
|
||||||
public Mono<Map<String, String>> startSessionAndGetReplies(@Valid @RequestBody QuickReplyScreenRequestDTO request) {
|
public Mono<DetectIntentResponseDTO> startSessionAndGetReplies(@Valid @RequestBody QuickReplyScreenRequestDTO request) {
|
||||||
return quickRepliesManagerService.startQuickReplySession(request)
|
return quickRepliesManagerService.startQuickReplySession(request)
|
||||||
.map(response -> Map.of("responseId", response.responseId()))
|
|
||||||
.doOnSuccess(response -> logger.info("Successfully processed quick reply request"))
|
.doOnSuccess(response -> logger.info("Successfully processed quick reply request"))
|
||||||
.doOnError(error -> logger.error("Error processing quick reply request: {}", error.getMessage(), error));
|
.doOnError(error -> logger.error("Error processing quick reply request: {}", error.getMessage(), error));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class ConversationContextMapper {
|
|||||||
|
|
||||||
private static final int MAX_HISTORY_BYTES = 50 * 1024; // 50 KB
|
private static final int MAX_HISTORY_BYTES = 50 * 1024; // 50 KB
|
||||||
|
|
||||||
|
private static final String NOTIFICATION_TEXT_PARAM = "notification_text";
|
||||||
|
|
||||||
public String toText(ConversationSessionDTO session, List<ConversationMessageDTO> messages) {
|
public String toText(ConversationSessionDTO session, List<ConversationMessageDTO> messages) {
|
||||||
if (messages == null || messages.isEmpty()) {
|
if (messages == null || messages.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
@@ -75,23 +77,34 @@ public class ConversationContextMapper {
|
|||||||
|
|
||||||
private String formatEntry(ConversationMessageDTO entry) {
|
private String formatEntry(ConversationMessageDTO entry) {
|
||||||
String prefix = "User: ";
|
String prefix = "User: ";
|
||||||
|
String content = entry.text();
|
||||||
|
|
||||||
if (entry.type() != null) {
|
if (entry.type() != null) {
|
||||||
switch (entry.type()) {
|
switch (entry.type()) {
|
||||||
case MessageType.AGENT:
|
case AGENT:
|
||||||
prefix = "Agent: ";
|
prefix = "Agent: ";
|
||||||
break;
|
break;
|
||||||
case MessageType.SYSTEM:
|
case SYSTEM:
|
||||||
|
prefix = "System: ";
|
||||||
|
// fix: add notification in the conversation.
|
||||||
|
if (entry.parameters() != null && entry.parameters().containsKey(NOTIFICATION_TEXT_PARAM)) {
|
||||||
|
Object paramText = entry.parameters().get(NOTIFICATION_TEXT_PARAM);
|
||||||
|
if (paramText != null && !paramText.toString().isBlank()) {
|
||||||
|
content = paramText.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LLM:
|
||||||
prefix = "System: ";
|
prefix = "System: ";
|
||||||
break;
|
break;
|
||||||
case MessageType.USER:
|
case USER:
|
||||||
default:
|
default:
|
||||||
prefix = "User: ";
|
prefix = "User: ";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String text = prefix + entry.text();
|
String text = prefix + content;
|
||||||
|
|
||||||
if (entry.type() == MessageType.AGENT) {
|
if (entry.type() == MessageType.AGENT) {
|
||||||
text = cleanAgentMessage(text);
|
text = cleanAgentMessage(text);
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ public class MessageEntryFilter {
|
|||||||
yield CATEGORY_UNKNOWN;
|
yield CATEGORY_UNKNOWN;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
resultCategory = CATEGORY_CONVERSATION;
|
|
||||||
return resultCategory;
|
return resultCategory;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("An error occurred during Gemini content generation for message classification.", e);
|
logger.error("An error occurred during Gemini content generation for message classification.", e);
|
||||||
|
|||||||
@@ -161,41 +161,8 @@ public class ConversationManagerService {
|
|||||||
String userMessageText = request.queryInput().text().text();
|
String userMessageText = request.queryInput().text().text();
|
||||||
final ConversationContext context = new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
final ConversationContext context = new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
||||||
|
|
||||||
return handleMessageClassification(context, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<DetectIntentResponseDTO> handleMessageClassification(ConversationContext context,
|
|
||||||
DetectIntentRequestDTO request) {
|
|
||||||
final String userPhoneNumber = context.primaryPhoneNumber();
|
|
||||||
final String userMessageText = context.userMessageText();
|
|
||||||
|
|
||||||
return memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
|
||||||
.flatMap(session -> memoryStoreConversationService.getMessages(session.sessionId()).collectList()
|
|
||||||
.map(conversationContextMapper::toTextFromMessages)
|
|
||||||
.defaultIfEmpty("")
|
|
||||||
.flatMap(conversationHistory -> {
|
|
||||||
return memoryStoreNotificationService.getNotificationIdForPhone(userPhoneNumber)
|
|
||||||
.flatMap(notificationId -> memoryStoreNotificationService
|
|
||||||
.getCachedNotificationSession(notificationId))
|
|
||||||
.map(notificationSession -> notificationSession.notificaciones().stream()
|
|
||||||
.filter(notification -> "active".equalsIgnoreCase(notification.status()))
|
|
||||||
.max(java.util.Comparator.comparing(NotificationDTO::timestampCreacion))
|
|
||||||
.orElse(null))
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.flatMap((NotificationDTO notification) -> {
|
|
||||||
String notificationText = notificationContextMapper.toText(notification);
|
|
||||||
String classification = messageEntryFilter.classifyMessage(userMessageText,
|
|
||||||
notificationText, conversationHistory);
|
|
||||||
if (MessageEntryFilter.CATEGORY_NOTIFICATION.equals(classification)) {
|
|
||||||
return startNotificationConversation(context, request, notification);
|
|
||||||
} else {
|
|
||||||
return continueConversationFlow(context, request);
|
return continueConversationFlow(context, request);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.switchIfEmpty(continueConversationFlow(context, request));
|
|
||||||
}))
|
|
||||||
.switchIfEmpty(continueConversationFlow(context, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<DetectIntentResponseDTO> continueConversationFlow(ConversationContext context,
|
private Mono<DetectIntentResponseDTO> continueConversationFlow(ConversationContext context,
|
||||||
DetectIntentRequestDTO request) {
|
DetectIntentRequestDTO request) {
|
||||||
@@ -357,7 +324,6 @@ public class ConversationManagerService {
|
|||||||
String resolvedContext = notificationContextResolver.resolveContext(userMessageText,
|
String resolvedContext = notificationContextResolver.resolveContext(userMessageText,
|
||||||
notificationText, conversationHistory, filteredParams.toString(), userId, sessionId,
|
notificationText, conversationHistory, filteredParams.toString(), userId, sessionId,
|
||||||
userPhoneNumber);
|
userPhoneNumber);
|
||||||
|
|
||||||
if (!resolvedContext.trim().toUpperCase().contains(NotificationContextResolver.CATEGORY_DIALOGFLOW)) {
|
if (!resolvedContext.trim().toUpperCase().contains(NotificationContextResolver.CATEGORY_DIALOGFLOW)) {
|
||||||
String uuid = UUID.randomUUID().toString();
|
String uuid = UUID.randomUUID().toString();
|
||||||
llmResponseTunerService.setValue(uuid, resolvedContext).subscribe();
|
llmResponseTunerService.setValue(uuid, resolvedContext).subscribe();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.lang.reflect.Method;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -118,4 +119,30 @@ public class ConversationContextMapperTest {
|
|||||||
private ConversationMessageDTO createMessage(String text, MessageType type) {
|
private ConversationMessageDTO createMessage(String text, MessageType type) {
|
||||||
return new ConversationMessageDTO(type, Instant.now(), text, null, null);
|
return new ConversationMessageDTO(type, Instant.now(), text, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testToTextFromMessages_SystemNotification_ShouldUseParamText() {
|
||||||
|
ConversationContextMapper mapper = new ConversationContextMapper();
|
||||||
|
|
||||||
|
Map<String, Object> params = new java.util.HashMap<>();
|
||||||
|
params.put("notification_text", "Tu estado de cuenta está listo");
|
||||||
|
|
||||||
|
ConversationMessageDTO systemMessage = new ConversationMessageDTO(
|
||||||
|
MessageType.SYSTEM,
|
||||||
|
Instant.now(),
|
||||||
|
"NOTIFICATION",
|
||||||
|
params,
|
||||||
|
"whatsapp"
|
||||||
|
);
|
||||||
|
|
||||||
|
List<ConversationMessageDTO> messages = new java.util.ArrayList<>();
|
||||||
|
messages.add(systemMessage);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
String result = mapper.toTextFromMessages(messages);
|
||||||
|
System.out.println(result);
|
||||||
|
// THEN
|
||||||
|
assertEquals("System: Tu estado de cuenta está listo", result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user