Initial commit
This commit is contained in:
44
src/main/java/com/example/Orchestrator.java
Normal file
44
src/main/java/com/example/Orchestrator.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.example;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
|
||||
|
||||
import com.google.api.gax.core.CredentialsProvider;
|
||||
import com.google.api.gax.core.NoCredentialsProvider;
|
||||
import com.google.cloud.spring.data.firestore.repository.config.EnableReactiveFirestoreRepositories;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableTransactionManagement
|
||||
@EnableReactiveFirestoreRepositories(basePackages = "com.example.repository")
|
||||
public class Orchestrator {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "spring.cloud.gcp.firestore.emulator.enabled",
|
||||
havingValue = "true")
|
||||
public CredentialsProvider googleCredentials() {
|
||||
return NoCredentialsProvider.create();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Orchestrator.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> indexRouter(
|
||||
@Value("classpath:/static/index.html") final Resource indexHtml) {
|
||||
return route(GET("/"), request -> ok().contentType(MediaType.TEXT_HTML).bodyValue(indexHtml));
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/example/config/OpenApiConfig.java
Normal file
23
src/main/java/com/example/config/OpenApiConfig.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.example.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Google Middleware API")
|
||||
.version("1.0")
|
||||
.description("API documentation. " +
|
||||
"It provides functionalities for user management, file storage, and more.")
|
||||
.termsOfService("http://swagger.io/terms/")
|
||||
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/example/config/RedisConfig.java
Normal file
43
src/main/java/com/example/config/RedisConfig.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.example.config;
|
||||
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisTemplate<String, ConversationSessionDTO> reactiveConversationRedisTemplate(
|
||||
ReactiveRedisConnectionFactory factory) {
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
Jackson2JsonRedisSerializer<ConversationSessionDTO> serializer =
|
||||
new Jackson2JsonRedisSerializer<>(objectMapper, ConversationSessionDTO.class);
|
||||
|
||||
return new ReactiveRedisTemplate<>(factory, RedisSerializationContext
|
||||
.<String, ConversationSessionDTO>newSerializationContext(new StringRedisSerializer())
|
||||
.value(serializer)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReactiveRedisTemplate<String, String> reactiveStringRedisTemplate(
|
||||
ReactiveRedisConnectionFactory factory) {
|
||||
return new ReactiveRedisTemplate<>(factory, RedisSerializationContext
|
||||
.<String, String>newSerializationContext(new StringRedisSerializer())
|
||||
.value(new StringRedisSerializer())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/example/config/VertexAIConfig.java
Normal file
33
src/main/java/com/example/config/VertexAIConfig.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.example.config;
|
||||
|
||||
import com.google.genai.Client;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
public class VertexAIConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VertexAIConfig.class);
|
||||
|
||||
@Value("${google.cloud.project}")
|
||||
private String projectId;
|
||||
|
||||
@Value("${google.cloud.location}")
|
||||
private String location;
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public Client geminiClient() throws IOException {
|
||||
logger.info("Initializing Google Gen AI Client. Project: {}, Location: {}", projectId, location);
|
||||
return Client.builder()
|
||||
.project(projectId)
|
||||
.location(location)
|
||||
.vertexAI(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.DetectIntentResponseDTO;
|
||||
import com.example.service.ConversationManagerService;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
public class ConversationController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConversationController.class);
|
||||
private final ConversationManagerService conversationManagerService;
|
||||
|
||||
public ConversationController(ConversationManagerService conversationManagerService) {
|
||||
this.conversationManagerService = conversationManagerService;
|
||||
}
|
||||
|
||||
@PostMapping("/dialogflow/detect-intent")
|
||||
public Mono<DetectIntentResponseDTO> detectIntent(@Valid @RequestBody DetectIntentRequestDTO request) {
|
||||
logger.info("Received request for session: {}", request.sessionId());
|
||||
return conversationManagerService.manageConversation(request)
|
||||
.doOnSuccess(response -> logger.info("Successfully processed direct Dialogflow request for session: {}", request.sessionId()))
|
||||
.doOnError(error -> logger.error("Error processing direct Dialogflow request for session {}: {}", request.sessionId(), error.getMessage(), error));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.dto.gemini.ConversationSummaryRequest;
|
||||
import com.example.dto.gemini.ConversationSummaryResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import com.example.service.ConversationSummaryService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/summary")
|
||||
public class ConversationSummaryController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConversationSummaryController.class);
|
||||
private final ConversationSummaryService conversationSummaryService;
|
||||
|
||||
public ConversationSummaryController(ConversationSummaryService conversationSummaryService) {
|
||||
this.conversationSummaryService = conversationSummaryService;
|
||||
}
|
||||
|
||||
@PostMapping("/conversation")
|
||||
public ResponseEntity<ConversationSummaryResponse> summarizeConversation(
|
||||
@Valid @RequestBody ConversationSummaryRequest request) {
|
||||
|
||||
logger.info("Received request to summarize conversation for session ID: {}",
|
||||
request.sessionId());
|
||||
|
||||
ConversationSummaryResponse response = conversationSummaryService.summarizeConversation(request);
|
||||
|
||||
if (response.summaryText() != null &&
|
||||
(response.summaryText().contains("Error:") ||
|
||||
response.summaryText().contains("Failed:") ||
|
||||
response.summaryText().contains("not found") ||
|
||||
response.summaryText().contains("No conversation provided"))) {
|
||||
logger.error("Summarization failed for session ID {}: {}", request.sessionId(), response.summaryText());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||
} else {
|
||||
logger.info("Successfully processed summarization request for session ID: {}", request.sessionId());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/example/dto/base/BaseRequest.java
Normal file
17
src/main/java/com/example/dto/base/BaseRequest.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.dto.base;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "type"
|
||||
)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = NotificationRequest.class, name = "NOTIFICATION"),
|
||||
})
|
||||
public sealed interface BaseRequest
|
||||
permits NotificationRequest{
|
||||
String type();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.dto.base;
|
||||
|
||||
public record ConversationContext(
|
||||
String userId,
|
||||
String sessionId,
|
||||
String userMessageText,
|
||||
String primaryPhoneNumber
|
||||
) {}
|
||||
26
src/main/java/com/example/dto/base/NotificationRequest.java
Normal file
26
src/main/java/com/example/dto/base/NotificationRequest.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.example.dto.base;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record NotificationRequest(
|
||||
@JsonProperty("requestId") String requestId,
|
||||
@JsonProperty("sessionId") String sessionId,
|
||||
@JsonProperty("mensaje") String message,
|
||||
@JsonProperty("SIC") String SIC,
|
||||
@Valid Usuario usuario,
|
||||
@JsonProperty("pantalla_contexto") String pantallaContexto,
|
||||
@JsonProperty("canal") String canal
|
||||
) implements BaseRequest {
|
||||
@Override
|
||||
public String type() {
|
||||
return "NOTIFICATION";
|
||||
}
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record Usuario(
|
||||
String telefono,
|
||||
String nickname
|
||||
) {
|
||||
}
|
||||
}
|
||||
9
src/main/java/com/example/dto/base/UsuarioDTO.java
Normal file
9
src/main/java/com/example/dto/base/UsuarioDTO.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.example.dto.base;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record UsuarioDTO(
|
||||
@JsonProperty("telefono") @NotBlank String telefono,
|
||||
@JsonProperty("nickname") String nickname
|
||||
) {}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ConversationEntryDTO(
|
||||
ConversationEntryType type,
|
||||
Instant timestamp,
|
||||
String text,
|
||||
String intentDisplayName,
|
||||
Map<String, Object> parameters,
|
||||
String webhookStatus,
|
||||
String canal
|
||||
) {
|
||||
public static ConversationEntryDTO forUser(String text) {
|
||||
return new ConversationEntryDTO(ConversationEntryType.USER_MESSAGE, Instant.now(),
|
||||
text, null, null, null, null);
|
||||
}
|
||||
|
||||
public static ConversationEntryDTO forAgent(QueryResultDTO agentQueryResult) {
|
||||
String fulfillmentText = (agentQueryResult != null && agentQueryResult.responseText() != null) ? agentQueryResult.responseText() : "";
|
||||
|
||||
return new ConversationEntryDTO(
|
||||
ConversationEntryType.AGENT_RESPONSE,
|
||||
Instant.now(),
|
||||
fulfillmentText,
|
||||
null,
|
||||
agentQueryResult.parameters(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
public enum ConversationEntryType {
|
||||
USER_MESSAGE,
|
||||
AGENT_RESPONSE
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ConversationSessionDTO(
|
||||
String sessionId,
|
||||
String userId,
|
||||
String telefono,
|
||||
Instant createdAt,
|
||||
Instant lastModified,
|
||||
List<ConversationEntryDTO> entries
|
||||
) {
|
||||
public ConversationSessionDTO(String sessionId, String userId, String telefono, Instant createdAt, Instant lastModified, List<ConversationEntryDTO> entries) {
|
||||
this.sessionId = sessionId;
|
||||
this.userId = userId;
|
||||
this.telefono = telefono;
|
||||
this.createdAt = createdAt;
|
||||
this.lastModified = lastModified;
|
||||
this.entries = Collections.unmodifiableList(new ArrayList<>(entries));
|
||||
}
|
||||
|
||||
public static ConversationSessionDTO create(String sessionId, String userId, String telefono) {
|
||||
Instant now = Instant.now();
|
||||
return new ConversationSessionDTO(sessionId, userId, telefono, now, now, Collections.emptyList());
|
||||
}
|
||||
|
||||
public ConversationSessionDTO withAddedEntry(ConversationEntryDTO newEntry) {
|
||||
List<ConversationEntryDTO> updatedEntries = new ArrayList<>(this.entries);
|
||||
updatedEntries.add(newEntry);
|
||||
return new ConversationSessionDTO(this.sessionId, this.userId, this.telefono, this.createdAt, Instant.now(), updatedEntries);
|
||||
}
|
||||
|
||||
public ConversationSessionDTO withTelefono(String newTelefono) {
|
||||
if (newTelefono != null && !newTelefono.equals(this.telefono)) {
|
||||
return new ConversationSessionDTO(this.sessionId, this.userId, newTelefono, this.createdAt, this.lastModified, this.entries);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
import com.example.dto.base.UsuarioDTO;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record DetectIntentRequestDTO(
|
||||
@JsonProperty("session") String sessionId,
|
||||
@JsonProperty("queryInput") QueryInputDTO queryInput,
|
||||
@JsonProperty("queryParams") QueryParamsDTO queryParams,
|
||||
@JsonProperty("usuario") UsuarioDTO usuario,
|
||||
String userId
|
||||
) {
|
||||
|
||||
public DetectIntentRequestDTO withSessionId(String newSessionId) {
|
||||
return new DetectIntentRequestDTO(
|
||||
newSessionId,
|
||||
this.queryInput(),
|
||||
this.queryParams(),
|
||||
this.usuario(),
|
||||
this.userId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record DetectIntentResponseDTO(
|
||||
@JsonProperty("responseId") String responseId,
|
||||
@JsonProperty("queryResult") QueryResultDTO queryResult
|
||||
) {}
|
||||
6
src/main/java/com/example/dto/dialogflow/IntentDTO.java
Normal file
6
src/main/java/com/example/dto/dialogflow/IntentDTO.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
public record IntentDTO(
|
||||
String name,
|
||||
String displayName
|
||||
) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
public record QueryInputDTO(TextInputDTO text, String languageCode) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
import java.util.Map;
|
||||
public record QueryParamsDTO(Map<String, Object> parameters) {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Map;
|
||||
|
||||
public record QueryResultDTO(
|
||||
@JsonProperty("responseText") String responseText,
|
||||
@JsonProperty("parameters") Map<String, Object> parameters
|
||||
) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.dto.dialogflow;
|
||||
|
||||
public record TextInputDTO(String text) {}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.dto.gemini;
|
||||
|
||||
import com.example.dto.dialogflow.ConversationEntryType;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.cloud.Timestamp;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ConversationEntrySummaryDTO(
|
||||
@JsonProperty("text") String text,
|
||||
@JsonProperty("timestamp") Timestamp timestamp,
|
||||
Optional<ConversationEntryType> type,
|
||||
@JsonProperty("intentDisplayName") String intentDisplayName,
|
||||
@JsonProperty("parameters") Map<String, Object> parameters,
|
||||
@JsonProperty("webhookStatus") String webhookStatus,
|
||||
@JsonProperty("canal") String canal
|
||||
) {
|
||||
@JsonCreator
|
||||
public ConversationEntrySummaryDTO(
|
||||
@JsonProperty("text") String text,
|
||||
@JsonProperty("timestamp") Timestamp timestamp,
|
||||
@JsonProperty("type") String typeString,
|
||||
@JsonProperty("intentDisplayName") String intentDisplayName,
|
||||
@JsonProperty("parameters") Map<String, Object> parameters,
|
||||
@JsonProperty("webhookStatus") String webhookStatus,
|
||||
@JsonProperty("canal") String canal
|
||||
) {
|
||||
this(
|
||||
text,
|
||||
timestamp,
|
||||
Optional.ofNullable(typeString).map(t -> {
|
||||
try {
|
||||
return ConversationEntryType.valueOf(t);
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println("Warning: Invalid ConversationEntryType string during deserialization: " + t);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
intentDisplayName,
|
||||
parameters,
|
||||
webhookStatus,
|
||||
canal
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.dto.gemini;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.cloud.Timestamp;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ConversationSessionSummaryDTO(
|
||||
@JsonProperty("sessionId") String sessionId,
|
||||
@JsonProperty("userId") String userId,
|
||||
@JsonProperty("startTime") Timestamp startTime,
|
||||
@JsonProperty("lastUpdated") Timestamp lastUpdated,
|
||||
@JsonProperty("entries") List<ConversationEntrySummaryDTO> entries
|
||||
) {
|
||||
@JsonCreator
|
||||
public ConversationSessionSummaryDTO(
|
||||
@JsonProperty("sessionId") String sessionId,
|
||||
@JsonProperty("userId") String userId,
|
||||
@JsonProperty("startTime") Timestamp startTime,
|
||||
@JsonProperty("lastUpdated") Timestamp lastUpdated,
|
||||
@JsonProperty("entries") List<ConversationEntrySummaryDTO> entries
|
||||
) {
|
||||
this.sessionId = sessionId;
|
||||
this.userId = userId;
|
||||
this.startTime = startTime;
|
||||
this.lastUpdated = lastUpdated;
|
||||
this.entries = entries != null ? entries : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.dto.gemini;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ConversationSummaryRequest(
|
||||
@NotBlank(message = "Session ID is required.")
|
||||
String sessionId,
|
||||
@NotBlank(message = "Prompt for summarization is required.")
|
||||
String prompt,
|
||||
@Min(value = 0, message = "Temperature must be between 0.0 and 1.0.")
|
||||
@Max(value = 1, message = "Temperature must be between 0.0 and 1.0.")
|
||||
Float temperature,
|
||||
@Min(value = 1, message = "Max Output Tokens must be at least 1.")
|
||||
Integer maxOutputTokens,
|
||||
@NotBlank(message = "model is required.")
|
||||
String modelName
|
||||
) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.dto.gemini;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ConversationSummaryResponse(
|
||||
@NotBlank
|
||||
String summaryText
|
||||
) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.exception;
|
||||
|
||||
public class DialogflowClientException extends RuntimeException {
|
||||
|
||||
public DialogflowClientException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DialogflowClientException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.exception;
|
||||
|
||||
public class FirestorePersistenceException extends RuntimeException {
|
||||
|
||||
public FirestorePersistenceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FirestorePersistenceException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentRequestDTO;
|
||||
import com.example.util.ProtobufUtil;
|
||||
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
|
||||
import com.google.cloud.dialogflow.cx.v3.QueryInput;
|
||||
import com.google.cloud.dialogflow.cx.v3.QueryParameters;
|
||||
import com.google.cloud.dialogflow.cx.v3.TextInput;
|
||||
import com.google.protobuf.Struct;
|
||||
import com.google.protobuf.Value;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class DialogflowRequestMapper {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DialogflowRequestMapper.class);
|
||||
|
||||
@org.springframework.beans.factory.annotation.Value("${dialogflow.default-language-code:es}") String defaultLanguageCode;
|
||||
|
||||
public DetectIntentRequest.Builder mapToDetectIntentRequestBuilder(DetectIntentRequestDTO requestDto) {
|
||||
Objects.requireNonNull(requestDto, "DetectIntentRequestDTO cannot be null for mapping.");
|
||||
|
||||
logger.debug("Building partial Dialogflow CX DetectIntentRequest Protobuf Builder from DTO (only QueryInput and QueryParams).");
|
||||
|
||||
// Build QueryInput from the DTO's text and language code
|
||||
QueryInput.Builder queryInputBuilder = QueryInput.newBuilder();
|
||||
if (requestDto.queryInput() != null && requestDto.queryInput().text() != null &&
|
||||
requestDto.queryInput().text().text() != null && !requestDto.queryInput().text().text().trim().isEmpty()) {
|
||||
|
||||
queryInputBuilder.setText(TextInput.newBuilder()
|
||||
.setText(requestDto.queryInput().text().text())
|
||||
.build());
|
||||
queryInputBuilder.setLanguageCode(requestDto.queryInput().languageCode() != null ? requestDto.queryInput().languageCode() : defaultLanguageCode);
|
||||
} else {
|
||||
logger.error("Dialogflow query input text is required for building Protobuf query input.");
|
||||
throw new IllegalArgumentException("Dialogflow query input text is required.");
|
||||
}
|
||||
|
||||
// Build QueryParameters from the DTO's parameters map
|
||||
QueryParameters.Builder queryParametersBuilder = QueryParameters.newBuilder();
|
||||
Struct.Builder paramsStructBuilder = Struct.newBuilder();
|
||||
// Add existing parameters from DTO's queryParams
|
||||
if (requestDto.queryParams() != null && requestDto.queryParams().parameters() != null) {
|
||||
for (Map.Entry<String, Object> entry : requestDto.queryParams().parameters().entrySet()) {
|
||||
Value protobufValue = ProtobufUtil.convertJavaObjectToProtobufValue(entry.getValue());
|
||||
paramsStructBuilder.putFields(entry.getKey(), protobufValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestDto.usuario() != null && requestDto.usuario().telefono() != null && !requestDto.usuario().telefono().trim().isEmpty()) {
|
||||
paramsStructBuilder.putFields("telefono", Value.newBuilder().setStringValue(requestDto.usuario().telefono()).build());
|
||||
logger.debug("Added 'telefono' as a query parameter: {}", requestDto.usuario().telefono());
|
||||
}
|
||||
|
||||
if (paramsStructBuilder.getFieldsCount() > 0) {
|
||||
queryParametersBuilder.setParameters(paramsStructBuilder.build());
|
||||
logger.debug("Custom parameters (including telefono if present) added to Protobuf request builder.");
|
||||
} else {
|
||||
logger.debug("No custom parameters provided in DTO (or no telefono in usuario).");
|
||||
}
|
||||
|
||||
DetectIntentRequest.Builder detectIntentRequestBuilder = DetectIntentRequest.newBuilder()
|
||||
.setQueryInput(queryInputBuilder.build());
|
||||
|
||||
if (queryParametersBuilder.hasParameters()) {
|
||||
detectIntentRequestBuilder.setQueryParams(queryParametersBuilder.build());
|
||||
}
|
||||
|
||||
logger.debug("Finished building partial DetectIntentRequest Protobuf Builder.");
|
||||
return detectIntentRequestBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.QueryResultDTO;
|
||||
import com.google.cloud.dialogflow.cx.v3.QueryResult;
|
||||
import com.google.cloud.dialogflow.cx.v3.ResponseMessage;
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentResponse;
|
||||
import com.example.util.ProtobufUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DialogflowResponseMapper {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DialogflowResponseMapper.class);
|
||||
|
||||
public DetectIntentResponseDTO mapFromDialogflowResponse(DetectIntentResponse response, String sessionId) {
|
||||
|
||||
logger.info("Starting mapping of Dialogflow DetectIntentResponse for session: {}", sessionId);
|
||||
|
||||
String responseId = response.getResponseId();
|
||||
QueryResult dfQueryResult = response.getQueryResult();
|
||||
logger.debug("Extracted QueryResult object for session: {}", sessionId);
|
||||
|
||||
StringBuilder responseTextBuilder = new StringBuilder();
|
||||
if (dfQueryResult.getResponseMessagesList().isEmpty()) {
|
||||
logger.debug("No response messages found in QueryResult for session: {}", sessionId);
|
||||
}
|
||||
|
||||
for (ResponseMessage message : dfQueryResult.getResponseMessagesList()) {
|
||||
if (message.hasText()) {
|
||||
logger.debug("Processing text response message for session: {}", sessionId);
|
||||
for (String text : message.getText().getTextList()) {
|
||||
if (responseTextBuilder.length() > 0) {
|
||||
responseTextBuilder.append(" ");
|
||||
}
|
||||
responseTextBuilder.append(text);
|
||||
logger.debug("Appended text segment: '{}' to fulfillment text for session: {}", text, sessionId);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Skipping non-text response message type: {} for session: {}", message.getMessageCase(), sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
String responseText = responseTextBuilder.toString().trim();
|
||||
|
||||
Map<String, Object> parameters = Collections.emptyMap();
|
||||
if (dfQueryResult.hasParameters()) {
|
||||
parameters = dfQueryResult.getParameters().getFieldsMap().entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> ProtobufUtil.convertProtobufValueToJavaObject(entry.getValue()),
|
||||
(oldValue, newValue) -> oldValue, // In case of duplicate keys, keep the old value
|
||||
LinkedHashMap::new // Preserve insertion order
|
||||
));
|
||||
logger.debug("Extracted parameters: {} for session: {}", parameters, sessionId);
|
||||
} else {
|
||||
logger.debug("No parameters found in QueryResult for session: {}. Using empty map.", sessionId);
|
||||
}
|
||||
|
||||
QueryResultDTO ourQueryResult = new QueryResultDTO(responseText, parameters);
|
||||
logger.debug("Internal QueryResult DTO created for session: {}. Details: {}", sessionId, ourQueryResult);
|
||||
|
||||
DetectIntentResponseDTO finalResponse = new DetectIntentResponseDTO(responseId, ourQueryResult);
|
||||
logger.info("Finished mapping DialogflowDetectIntentResponse for session: {}. Full response ID: {}", sessionId, responseId);
|
||||
return finalResponse;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationEntryType;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
import com.google.cloud.Timestamp;
|
||||
import com.google.cloud.firestore.FieldValue;
|
||||
import com.google.cloud.firestore.DocumentSnapshot;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class FirestoreConversationMapper {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FirestoreConversationMapper.class);
|
||||
|
||||
public Map<String, Object> createUpdateMapForSingleEntry(ConversationEntryDTO newEntry) {
|
||||
Map<String, Object> updates = new HashMap<>();
|
||||
// Convert Instant to Firestore Timestamp
|
||||
Map<String, Object> entryMap = toFirestoreEntryMap(newEntry);
|
||||
updates.put("entries", FieldValue.arrayUnion(entryMap));
|
||||
// Convert Instant to Firestore Timestamp
|
||||
updates.put("lastModified", Timestamp.of(java.util.Date.from(Instant.now())));
|
||||
return updates;
|
||||
}
|
||||
|
||||
public Map<String, Object> createNewSessionMapForSingleEntry(String sessionId, String userId, String telefono, ConversationEntryDTO initialEntry) {
|
||||
Map<String, Object> sessionMap = new HashMap<>();
|
||||
sessionMap.put("sessionId", sessionId);
|
||||
sessionMap.put("userId", userId);
|
||||
|
||||
if (telefono != null && !telefono.trim().isEmpty()) {
|
||||
sessionMap.put("telefono", telefono);
|
||||
} else {
|
||||
sessionMap.put("telefono", null);
|
||||
}
|
||||
|
||||
// Convert Instant to Firestore Timestamp
|
||||
sessionMap.put("createdAt", Timestamp.of(java.util.Date.from(Instant.now())));
|
||||
sessionMap.put("lastModified", Timestamp.of(java.util.Date.from(Instant.now())));
|
||||
|
||||
List<Map<String, Object>> entriesList = new ArrayList<>();
|
||||
entriesList.add(toFirestoreEntryMap(initialEntry));
|
||||
sessionMap.put("entries", entriesList);
|
||||
|
||||
return sessionMap;
|
||||
}
|
||||
|
||||
private Map<String, Object> toFirestoreEntryMap(ConversationEntryDTO entry) {
|
||||
Map<String, Object> entryMap = new HashMap<>();
|
||||
entryMap.put("type", entry.type().name());
|
||||
entryMap.put("text", entry.text());
|
||||
// Convert Instant to Firestore Timestamp for storage
|
||||
entryMap.put("timestamp", Timestamp.of(java.util.Date.from(entry.timestamp())));
|
||||
|
||||
if (entry.intentDisplayName() != null) {
|
||||
entryMap.put("intentDisplayName", entry.intentDisplayName());
|
||||
}
|
||||
if (entry.parameters() != null && !entry.parameters().isEmpty()) {
|
||||
entryMap.put("parameters", entry.parameters());
|
||||
}
|
||||
if (entry.webhookStatus() != null) {
|
||||
entryMap.put("webhookStatus", entry.webhookStatus());
|
||||
}
|
||||
if (entry.canal() != null) {
|
||||
entryMap.put("canal", entry.canal());
|
||||
}
|
||||
return entryMap;
|
||||
}
|
||||
|
||||
public ConversationSessionDTO mapFirestoreDocumentToConversationSessionDTO(DocumentSnapshot documentSnapshot) {
|
||||
if (!documentSnapshot.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String sessionId = documentSnapshot.getString("sessionId");
|
||||
String userId = documentSnapshot.getString("userId");
|
||||
String telefono = documentSnapshot.getString("telefono");
|
||||
|
||||
// Convert Firestore Timestamp to Instant
|
||||
Timestamp createdAtFirestore = documentSnapshot.getTimestamp("createdAt");
|
||||
Instant createdAt = (createdAtFirestore != null) ? createdAtFirestore.toDate().toInstant() : null;
|
||||
|
||||
// Convert Firestore Timestamp to Instant
|
||||
Timestamp lastModifiedFirestore = documentSnapshot.getTimestamp("lastModified");
|
||||
Instant lastModified = (lastModifiedFirestore != null) ? lastModifiedFirestore.toDate().toInstant() : null;
|
||||
|
||||
List<Map<String, Object>> rawEntries = (List<Map<String, Object>>) documentSnapshot.get("entries");
|
||||
List<ConversationEntryDTO> entries = new ArrayList<>();
|
||||
if (rawEntries != null) {
|
||||
entries = rawEntries.stream()
|
||||
.map(this::mapFirestoreEntryMapToConversationEntryDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return new ConversationSessionDTO(sessionId, userId, telefono, createdAt, lastModified, entries);
|
||||
}
|
||||
|
||||
|
||||
private ConversationEntryDTO mapFirestoreEntryMapToConversationEntryDTO(Map<String, Object> entryMap) {
|
||||
ConversationEntryType type = null;
|
||||
Object typeObj = entryMap.get("type");
|
||||
if (typeObj instanceof String) {
|
||||
try {
|
||||
type = ConversationEntryType.valueOf((String) typeObj);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Unknown ConversationEntryType encountered: {}. Setting type to null.", typeObj);
|
||||
}
|
||||
}
|
||||
|
||||
String text = (String) entryMap.get("text");
|
||||
|
||||
// Convert Firestore Timestamp to Instant
|
||||
Timestamp timestampFirestore = (Timestamp) entryMap.get("timestamp");
|
||||
Instant timestamp = (timestampFirestore != null) ? timestampFirestore.toDate().toInstant() : null;
|
||||
|
||||
String intentDisplayName = (String) entryMap.get("intentDisplayName");
|
||||
Map<String, Object> parameters = (Map<String, Object>) entryMap.get("parameters");
|
||||
String webhookStatus = (String) entryMap.get("webhookStatus");
|
||||
String canal = (String) entryMap.get("canal");
|
||||
|
||||
return new ConversationEntryDTO(type, timestamp, text, intentDisplayName, parameters, webhookStatus, canal);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.example.repository;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
||||
import com.google.api.core.ApiFuture;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import com.google.cloud.firestore.DocumentSnapshot;
|
||||
import com.google.cloud.firestore.Firestore;
|
||||
import com.google.cloud.firestore.WriteBatch;
|
||||
import com.google.cloud.firestore.WriteResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import com.example.util.FirestoreTimestampDeserializer;
|
||||
import com.example.util.FirestoreTimestampSerializer;
|
||||
|
||||
@Repository
|
||||
public class FirestoreBaseRepository {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FirestoreBaseRepository.class);
|
||||
|
||||
private final Firestore firestore;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${app.id:default-app-id}")
|
||||
private String appId;
|
||||
|
||||
public FirestoreBaseRepository(Firestore firestore, ObjectMapper objectMapper) {
|
||||
this.firestore = firestore;
|
||||
this.objectMapper = objectMapper;
|
||||
|
||||
// Register JavaTimeModule for standard java.time handling
|
||||
if (!objectMapper.findModules().stream().anyMatch(m -> m instanceof JavaTimeModule)) {
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
}
|
||||
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||
|
||||
// Register ParameterNamesModule, crucial for Java Records and classes compiled with -parameters
|
||||
if (!objectMapper.findModules().stream().anyMatch(m -> m instanceof ParameterNamesModule)) {
|
||||
objectMapper.registerModule(new ParameterNamesModule());
|
||||
}
|
||||
|
||||
// These specific Timestamp (Google Cloud) deserializers/serializers are for ObjectMapper
|
||||
// to handle com.google.cloud.Timestamp objects when mapping other types.
|
||||
// They are generally not the cause of the Redis deserialization error for Instant.
|
||||
|
||||
SimpleModule firestoreTimestampModule = new SimpleModule();
|
||||
firestoreTimestampModule.addDeserializer(com.google.cloud.Timestamp.class, new FirestoreTimestampDeserializer());
|
||||
firestoreTimestampModule.addSerializer(com.google.cloud.Timestamp.class, new FirestoreTimestampSerializer());
|
||||
objectMapper.registerModule(firestoreTimestampModule);
|
||||
|
||||
logger.info("FirestoreBaseRepository initialized with Firestore client and ObjectMapper. App ID will be: {}", appId);
|
||||
}
|
||||
|
||||
public DocumentReference getDocumentReference(String collectionPath, String documentId) {
|
||||
Objects.requireNonNull(collectionPath, "Collection path cannot be null.");
|
||||
Objects.requireNonNull(documentId, "Document ID cannot be null.");
|
||||
return firestore.collection(collectionPath).document(documentId);
|
||||
}
|
||||
|
||||
public <T> T getDocument(DocumentReference docRef, Class<T> clazz) throws InterruptedException, ExecutionException {
|
||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||
Objects.requireNonNull(clazz, "Class for mapping cannot be null.");
|
||||
ApiFuture<DocumentSnapshot> future = docRef.get();
|
||||
DocumentSnapshot document = future.get();
|
||||
if (document.exists()) {
|
||||
try {
|
||||
logger.debug("FirestoreBaseRepository: Raw document data for {}: {}", docRef.getPath(), document.getData());
|
||||
T result = objectMapper.convertValue(document.getData(), clazz);
|
||||
return result;
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Failed to convert Firestore document data to {}: {}", clazz.getName(), e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to convert Firestore document data to " + clazz.getName(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public DocumentSnapshot getDocumentSnapshot(DocumentReference docRef) throws ExecutionException, InterruptedException {
|
||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||
ApiFuture<DocumentSnapshot> future = docRef.get();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
public boolean documentExists(DocumentReference docRef) throws InterruptedException, ExecutionException {
|
||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||
ApiFuture<DocumentSnapshot> future = docRef.get();
|
||||
return future.get().exists();
|
||||
}
|
||||
|
||||
public void setDocument(DocumentReference docRef, Object data) throws InterruptedException, ExecutionException {
|
||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||
Objects.requireNonNull(data, "Data for setting document cannot be null.");
|
||||
ApiFuture<WriteResult> future = docRef.set(data);
|
||||
WriteResult writeResult = future.get();
|
||||
logger.debug("Document set: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
|
||||
}
|
||||
|
||||
public void updateDocument(DocumentReference docRef, Map<String, Object> updates) throws InterruptedException, ExecutionException {
|
||||
Objects.requireNonNull(docRef, "DocumentReference cannot be null.");
|
||||
Objects.requireNonNull(updates, "Updates map cannot be null.");
|
||||
ApiFuture<WriteResult> future = docRef.update(updates);
|
||||
WriteResult writeResult = future.get();
|
||||
logger.debug("Document updated: {} with update time: {}", docRef.getPath(), writeResult.getUpdateTime());
|
||||
}
|
||||
|
||||
public WriteBatch createBatch() {
|
||||
return firestore.batch();
|
||||
}
|
||||
|
||||
public void commitBatch(WriteBatch batch) throws InterruptedException, ExecutionException {
|
||||
Objects.requireNonNull(batch, "WriteBatch cannot be null.");
|
||||
batch.commit().get();
|
||||
logger.debug("Batch committed successfully.");
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.DetectIntentResponseDTO;
|
||||
import com.example.dto.base.ConversationContext;
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
import com.example.dto.base.UsuarioDTO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class ConversationManagerService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConversationManagerService.class);
|
||||
|
||||
private final DialogflowClientService dialogflowServiceClient;
|
||||
private final FirestoreConversationService firestoreConversationService;
|
||||
private final MemoryStoreConversationService memoryStoreConversationService;
|
||||
|
||||
public ConversationManagerService(
|
||||
DialogflowClientService dialogflowServiceClient,
|
||||
FirestoreConversationService firestoreConversationService,
|
||||
MemoryStoreConversationService memoryStoreConversationService) {
|
||||
this.dialogflowServiceClient = dialogflowServiceClient;
|
||||
this.firestoreConversationService = firestoreConversationService;
|
||||
this.memoryStoreConversationService = memoryStoreConversationService;
|
||||
}
|
||||
|
||||
public Mono<DetectIntentResponseDTO> manageConversation(DetectIntentRequestDTO request) {
|
||||
final ConversationContext context;
|
||||
try {
|
||||
context = resolveAndValidateRequest(request);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Validation error for incoming request: {}", e.getMessage());
|
||||
return Mono.error(e);
|
||||
}
|
||||
|
||||
final String userId = context.userId();
|
||||
final String userMessageText = context.userMessageText();
|
||||
final String userPhoneNumber = context.primaryPhoneNumber();
|
||||
|
||||
Mono<ConversationSessionDTO> sessionMono;
|
||||
if (userPhoneNumber != null && !userPhoneNumber.trim().isEmpty()) {
|
||||
logger.info("Checking for existing session for phone number: {}", userPhoneNumber);
|
||||
sessionMono = memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)
|
||||
.doOnNext(session -> logger.info("Found existing session {} for phone number {}", session.sessionId(), userPhoneNumber))
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
String newSessionId = UUID.randomUUID().toString();
|
||||
logger.info("No existing session found for phone number {}. Creating new session: {}", userPhoneNumber, newSessionId);
|
||||
return Mono.just(ConversationSessionDTO.create(newSessionId, userId, userPhoneNumber));
|
||||
}));
|
||||
} else {
|
||||
String newSessionId = UUID.randomUUID().toString();
|
||||
logger.warn("No phone number provided in request. Creating new session: {}", newSessionId);
|
||||
return Mono.error(new IllegalArgumentException("Phone number is required to manage conversation sessions."));
|
||||
}
|
||||
|
||||
return sessionMono.flatMap(session -> {
|
||||
final String finalSessionId = session.sessionId();
|
||||
logger.info("Managing conversation for resolved session: {}", finalSessionId);
|
||||
|
||||
ConversationEntryDTO userEntry = ConversationEntryDTO.forUser(userMessageText);
|
||||
DetectIntentRequestDTO updatedRequest = request.withSessionId(finalSessionId);
|
||||
return this.persistConversationTurn(userId, finalSessionId, userEntry, userPhoneNumber)
|
||||
.doOnSuccess(v -> logger.debug("User entry successfully persisted for session {}. Proceeding to Dialogflow...", finalSessionId))
|
||||
.doOnError(e -> logger.error("Error during user entry persistence for session {}: {}", finalSessionId, e.getMessage(), e))
|
||||
// After user entry persistence is complete (Mono<Void> emits 'onComplete'),
|
||||
// then proceed to call Dialogflow.
|
||||
.then(Mono.defer(() -> { // Use Mono.defer to ensure Dialogflow call is subscribed AFTER persistence
|
||||
// Call Dialogflow.
|
||||
return dialogflowServiceClient.detectIntent(finalSessionId, updatedRequest)
|
||||
.doOnSuccess(response -> {
|
||||
logger.debug("Received Dialogflow CX response for session {}. Initiating agent response persistence.", finalSessionId);
|
||||
ConversationEntryDTO agentEntry = ConversationEntryDTO.forAgent(response.queryResult());
|
||||
// Agent entry persistence can still be backgrounded via .subscribe()
|
||||
// if its completion isn't strictly required before returning the Dialogflow response.
|
||||
this.persistConversationTurn(userId, finalSessionId, agentEntry, userPhoneNumber).subscribe(
|
||||
v -> logger.debug("Background: Agent entry persistence initiated for session {}.", finalSessionId),
|
||||
e -> logger.error("Background: Error during agent entry persistence for session {}: {}", finalSessionId, e.getMessage(), e)
|
||||
);
|
||||
})
|
||||
.doOnError(error -> logger.error("Overall error during conversation management for session {}: {}", finalSessionId, error.getMessage(), error));
|
||||
}));
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
logger.error("Overall error handling conversation in ConversationManagerService: {}", e.getMessage(), e);
|
||||
return Mono.error(new RuntimeException("Failed to process conversation due to an internal error.", e));
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private Mono<Void> persistConversationTurn(String userId, String sessionId, ConversationEntryDTO entry, String userPhoneNumber) {
|
||||
logger.debug("Starting Write-Back persistence for session {}. Type: {}. Writing to Redis first.", sessionId, entry.type().name());
|
||||
|
||||
return memoryStoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
||||
.doOnSuccess(v -> {
|
||||
logger.info("Entry saved to Redis for session {}. Type: {}. Kicking off async Firestore write-back.", sessionId, entry.type().name());
|
||||
firestoreConversationService.saveEntry(userId, sessionId, entry, userPhoneNumber)
|
||||
.subscribe(
|
||||
fsVoid -> logger.debug("Asynchronously (Write-Back): Entry successfully saved to Firestore for session {}. Type: {}.",
|
||||
sessionId, entry.type().name()),
|
||||
fsError -> logger.error("Asynchronously (Write-Back): Failed to save entry to Firestore for session {}. Type: {}: {}",
|
||||
sessionId, entry.type().name(), fsError.getMessage(), fsError)
|
||||
);
|
||||
})
|
||||
.doOnError(e -> logger.error("Error during primary Redis write for session {}. Type: {}: {}", sessionId, entry.type().name(), e.getMessage(), e));
|
||||
}
|
||||
|
||||
private ConversationContext resolveAndValidateRequest(DetectIntentRequestDTO request) {
|
||||
String primaryPhoneNumber = Optional.ofNullable(request.usuario())
|
||||
.map(UsuarioDTO::telefono)
|
||||
.orElse(null);
|
||||
|
||||
if (primaryPhoneNumber == null || primaryPhoneNumber.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Phone number (telefono) is required in the 'usuario' field for conversation management.");
|
||||
}
|
||||
String resolvedUserId = request.userId();
|
||||
if (resolvedUserId == null || resolvedUserId.trim().isEmpty()) {
|
||||
resolvedUserId = "user_by_phone_" + primaryPhoneNumber.replaceAll("[^0-9]", ""); // Derive from phone number
|
||||
logger.warn("User ID not provided in request. Using derived ID from phone number: {}", resolvedUserId);
|
||||
}
|
||||
if (request.queryInput() == null || request.queryInput().text() == null || request.queryInput().text().text() == null || request.queryInput().text().text().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Dialogflow query input text is required.");
|
||||
}
|
||||
|
||||
String userMessageText = request.queryInput().text().text();
|
||||
return new ConversationContext(resolvedUserId, null, userMessageText, primaryPhoneNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.example.dto.gemini.ConversationSummaryRequest;
|
||||
import com.example.dto.gemini.ConversationSummaryResponse;
|
||||
import com.example.dto.gemini.ConversationSessionSummaryDTO;
|
||||
import com.example.dto.gemini.ConversationEntrySummaryDTO;
|
||||
import com.example.repository.FirestoreBaseRepository;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ConversationSummaryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConversationSummaryService.class);
|
||||
private final GeminiClientService geminiService;
|
||||
private final FirestoreBaseRepository firestoreBaseRepository;
|
||||
|
||||
private static final String CONVERSATION_COLLECTION_PATH_FORMAT = "artifacts/%s/conversations";
|
||||
|
||||
private static final String DEFAULT_GEMINI_MODEL_NAME = "gemini-2.0-flash-001";
|
||||
private static final Float DEFAULT_TEMPERATURE = 0.7f;
|
||||
private static final Integer DEFAULT_MAX_OUTPUT_TOKENS = 800;
|
||||
|
||||
public ConversationSummaryService(GeminiClientService geminiService, FirestoreBaseRepository firestoreBaseRepository) {
|
||||
this.geminiService = geminiService;
|
||||
this.firestoreBaseRepository = firestoreBaseRepository;
|
||||
}
|
||||
|
||||
public ConversationSummaryResponse summarizeConversation(ConversationSummaryRequest request) {
|
||||
if (request == null) {
|
||||
logger.warn("Summarization request is null.");
|
||||
return new ConversationSummaryResponse("Request cannot be null.");
|
||||
}
|
||||
if (request.sessionId() == null || request.sessionId().isBlank()) {
|
||||
logger.warn("Session ID is missing in the summarization request.");
|
||||
return new ConversationSummaryResponse("Session ID is required.");
|
||||
}
|
||||
if (request.prompt() == null || request.prompt().isBlank()) {
|
||||
logger.warn("Prompt for summarization is missing in the request.");
|
||||
return new ConversationSummaryResponse("Prompt for summarization is required.");
|
||||
}
|
||||
|
||||
String sessionId = request.sessionId();
|
||||
String summarizationPromptInstruction = request.prompt();
|
||||
|
||||
String actualModelName = (request.modelName() != null && !request.modelName().isBlank())
|
||||
? request.modelName() : DEFAULT_GEMINI_MODEL_NAME;
|
||||
Float actualTemperature = (request.temperature() != null)
|
||||
? request.temperature() : DEFAULT_TEMPERATURE;
|
||||
Integer actualMaxOutputTokens = (request.maxOutputTokens() != null)
|
||||
? request.maxOutputTokens() : DEFAULT_MAX_OUTPUT_TOKENS;
|
||||
|
||||
String collectionPath = String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
||||
String documentId = sessionId;
|
||||
logger.info("Fetching conversation from Firestore: Collection='{}', Document='{}'", collectionPath, documentId);
|
||||
|
||||
ConversationSessionSummaryDTO sessionSummary;
|
||||
try {
|
||||
DocumentReference docRef = firestoreBaseRepository.getDocumentReference(collectionPath, documentId);
|
||||
sessionSummary = firestoreBaseRepository.getDocument(docRef, ConversationSessionSummaryDTO.class);
|
||||
|
||||
logger.debug("Retrieved ConversationSessionSummaryDTO after Firestore fetch: sessionId={}, entries size={}",
|
||||
sessionSummary != null ? sessionSummary.sessionId() : "null",
|
||||
sessionSummary != null && sessionSummary.entries() != null ? sessionSummary.entries().size() : "N/A (entries list is null)");
|
||||
|
||||
if (sessionSummary == null) {
|
||||
logger.warn("Firestore document not found or could not be mapped: {}/{}", collectionPath, documentId);
|
||||
return new ConversationSummaryResponse("Conversation document not found for session ID: " + sessionId);
|
||||
}
|
||||
|
||||
List<ConversationEntrySummaryDTO> entries = sessionSummary.entries();
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
logger.warn("No conversation entries found in document {}/{} for session ID: {}",
|
||||
collectionPath, documentId, sessionId);
|
||||
return new ConversationSummaryResponse("No conversation messages found in the document for session ID: " + sessionId);
|
||||
}
|
||||
|
||||
List<String> conversationMessages = entries.stream()
|
||||
.map(entry -> {
|
||||
String type = entry.type().map(t -> t.name()).orElse("UNKNOWN_TYPE");
|
||||
String timestampString = entry.timestamp() != null ? entry.timestamp().toDate().toInstant().toString() : "UNKNOWN_TIMESTAMP";
|
||||
return String.format("[%s - %s] %s", type, timestampString, entry.text());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
String formattedConversation = String.join("\n", conversationMessages);
|
||||
String fullPromptForGemini = summarizationPromptInstruction + "\n\n" + formattedConversation;
|
||||
|
||||
logger.info("Sending summarization request to Gemini with custom prompt (first 200 chars): \n{}",
|
||||
fullPromptForGemini.substring(0, Math.min(fullPromptForGemini.length(), 200)) + "...");
|
||||
|
||||
String summaryText = geminiService.generateContent(
|
||||
fullPromptForGemini,
|
||||
actualTemperature,
|
||||
actualMaxOutputTokens,
|
||||
actualModelName
|
||||
);
|
||||
|
||||
if (summaryText == null || summaryText.trim().isEmpty()) {
|
||||
logger.warn("Gemini returned an empty or null summary for the conversation.");
|
||||
return new ConversationSummaryResponse("Could not generate a summary. The model returned no text.");
|
||||
}
|
||||
logger.info("Successfully generated summary for session ID: {}", sessionId);
|
||||
return new ConversationSummaryResponse(summaryText);
|
||||
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
logger.error("Error accessing Firestore for session ID {}: {}", sessionId, e.getMessage(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
return new ConversationSummaryResponse("Error accessing conversation data: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("An unexpected error occurred during summarization for session ID {}: {}", sessionId, e.getMessage(), e);
|
||||
return new ConversationSummaryResponse("An unexpected error occurred during summarization: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/main/java/com/example/service/DialogflowClientService.java
Normal file
120
src/main/java/com/example/service/DialogflowClientService.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.example.dto.dialogflow.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.DetectIntentResponseDTO;
|
||||
import com.example.mapper.DialogflowRequestMapper;
|
||||
import com.example.mapper.DialogflowResponseMapper;
|
||||
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.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;
|
||||
|
||||
@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);
|
||||
} 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);
|
||||
|
||||
// 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);
|
||||
})
|
||||
.onErrorMap(ApiException.class, e -> {
|
||||
logger.error("Dialogflow CX API error for session {}: status={}, message={}",
|
||||
sessionId, e.getStatusCode().getCode(), e.getMessage(), e);
|
||||
return new DialogflowClientException(
|
||||
"Dialogflow CX API error: " + e.getStatusCode().getCode() + " - " + e.getMessage(), e);
|
||||
})
|
||||
.onErrorMap(IOException.class, e -> {
|
||||
logger.error("IO error when calling Dialogflow CX for session {}: {}", sessionId, e.getMessage(),e);
|
||||
return new RuntimeException("IO error with Dialogflow CX API: " + e.getMessage(), e);
|
||||
})
|
||||
.map(dfResponse -> this.dialogflowResponseMapper.mapFromDialogflowResponse(dfResponse, sessionId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
import com.example.exception.FirestorePersistenceException;
|
||||
import com.example.mapper.FirestoreConversationMapper;
|
||||
import com.example.repository.FirestoreBaseRepository;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import com.google.cloud.firestore.DocumentSnapshot;
|
||||
import com.google.cloud.firestore.WriteBatch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Service
|
||||
public class FirestoreConversationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FirestoreConversationService.class);
|
||||
private static final String CONVERSATION_COLLECTION_PATH_FORMAT = "artifacts/%s/conversations";
|
||||
private final FirestoreBaseRepository firestoreBaseRepository;
|
||||
private final FirestoreConversationMapper firestoreConversationMapper;
|
||||
|
||||
public FirestoreConversationService(FirestoreBaseRepository firestoreBaseRepository, FirestoreConversationMapper firestoreConversationMapper) {
|
||||
this.firestoreBaseRepository = firestoreBaseRepository;
|
||||
this.firestoreConversationMapper = firestoreConversationMapper;
|
||||
}
|
||||
|
||||
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
||||
logger.info("Attempting to save conversation entry to Firestore for session {}. Type: {}", sessionId, newEntry.type().name());
|
||||
return Mono.fromRunnable(() -> {
|
||||
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
||||
WriteBatch batch = firestoreBaseRepository.createBatch();
|
||||
|
||||
try {
|
||||
if (firestoreBaseRepository.documentExists(sessionDocRef)) {
|
||||
// Update: Append the new entry using arrayUnion and update lastModified
|
||||
Map<String, Object> updates = firestoreConversationMapper.createUpdateMapForSingleEntry(newEntry);
|
||||
batch.update(sessionDocRef, updates);
|
||||
logger.info("Appending entry to existing conversation session for user {} and session {}. Type: {}", userId, sessionId, newEntry.type().name());
|
||||
} else {
|
||||
// Create: Start a new session with the first entry.
|
||||
// Pass userId and userPhoneNumber to the mapper to be stored as fields in the document.
|
||||
Map<String, Object> newSessionMap = firestoreConversationMapper.createNewSessionMapForSingleEntry(sessionId, userId, userPhoneNumber, newEntry);
|
||||
batch.set(sessionDocRef, newSessionMap);
|
||||
logger.info("Creating new conversation session with first entry for user {} and session {}. Type: {}", userId, sessionId, newEntry.type().name());
|
||||
}
|
||||
firestoreBaseRepository.commitBatch(batch);
|
||||
logger.info("Successfully committed batch for session {} to Firestore.", sessionId);
|
||||
} catch (ExecutionException e) {
|
||||
logger.error("Error saving conversation entry to Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||
throw new FirestorePersistenceException("Failed to save conversation entry to Firestore for session " + sessionId, e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.error("Thread interrupted while saving conversation entry to Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||
throw new FirestorePersistenceException("Saving conversation entry was interrupted for session " + sessionId, e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic()).then();
|
||||
}
|
||||
|
||||
public Mono<ConversationSessionDTO> getConversationSession(String userId, String sessionId) {
|
||||
logger.info("Attempting to retrieve conversation session for session {} (user ID {} for context).", sessionId, userId);
|
||||
return Mono.fromCallable(() -> {
|
||||
DocumentReference sessionDocRef = getSessionDocumentReference(sessionId);
|
||||
try {
|
||||
DocumentSnapshot documentSnapshot = firestoreBaseRepository.getDocumentSnapshot(sessionDocRef);
|
||||
if (documentSnapshot != null && documentSnapshot.exists()) {
|
||||
ConversationSessionDTO sessionDTO = firestoreConversationMapper.mapFirestoreDocumentToConversationSessionDTO(documentSnapshot);
|
||||
logger.info("Successfully retrieved and mapped conversation session for session {}.", sessionId);
|
||||
return sessionDTO;
|
||||
}
|
||||
logger.info("Conversation session not found for session {}.", sessionId);
|
||||
return null; // Or Mono.empty() if this method returned Mono<Optional<ConversationSessionDTO>>
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
logger.error("Error retrieving conversation session from Firestore for session {}: {}", sessionId, e.getMessage(), e);
|
||||
throw new FirestorePersistenceException("Failed to retrieve conversation session from Firestore for session " + sessionId, e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private String getConversationCollectionPath() {
|
||||
return String.format(CONVERSATION_COLLECTION_PATH_FORMAT, firestoreBaseRepository.getAppId());
|
||||
}
|
||||
|
||||
private DocumentReference getSessionDocumentReference(String sessionId) {
|
||||
String collectionPath = getConversationCollectionPath();
|
||||
return firestoreBaseRepository.getDocumentReference(collectionPath, sessionId);
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/example/service/GeminiClientService.java
Normal file
45
src/main/java/com/example/service/GeminiClientService.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.example.service;
|
||||
|
||||
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.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class GeminiClientService {
|
||||
|
||||
private final Client geminiClient;
|
||||
|
||||
public GeminiClientService(Client geminiClient) {
|
||||
this.geminiClient = geminiClient;
|
||||
}
|
||||
|
||||
public String generateContent(String prompt, Float temperature, Integer maxOutputTokens, String modelName) {
|
||||
try {
|
||||
Content content = Content.fromParts(Part.fromText(prompt));
|
||||
GenerateContentConfig config = GenerateContentConfig.builder()
|
||||
.temperature(temperature)
|
||||
.maxOutputTokens(maxOutputTokens)
|
||||
.build();
|
||||
|
||||
GenerateContentResponse response = geminiClient.models.generateContent(modelName, content, config);
|
||||
|
||||
if (response != null && response.text() != null) {
|
||||
return response.text();
|
||||
} else {
|
||||
return "No content generated or unexpected response structure.";
|
||||
}
|
||||
} catch (GenAiIOException e) {
|
||||
System.err.println("Gemini API I/O error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return "Error: An API communication issue occurred: " + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
System.err.println("An unexpected error occurred during Gemini content generation: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return "Error: An unexpected issue occurred during content generation.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.example.dto.dialogflow.ConversationEntryDTO;
|
||||
import com.example.dto.dialogflow.ConversationSessionDTO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.time.Duration;
|
||||
|
||||
@Service
|
||||
public class MemoryStoreConversationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MemoryStoreConversationService.class);
|
||||
private static final String SESSION_KEY_PREFIX = "conversation:session:";
|
||||
private static final String PHONE_TO_SESSION_KEY_PREFIX = "conversation:phone_to_session:";
|
||||
private static final Duration SESSION_TTL = Duration.ofHours(24);
|
||||
|
||||
private final ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate;
|
||||
private final ReactiveRedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
public MemoryStoreConversationService(
|
||||
ReactiveRedisTemplate<String, ConversationSessionDTO> redisTemplate,
|
||||
ReactiveRedisTemplate<String, String> stringRedisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
}
|
||||
|
||||
public Mono<Void> saveEntry(String userId, String sessionId, ConversationEntryDTO newEntry, String userPhoneNumber) {
|
||||
String sessionKey = SESSION_KEY_PREFIX + sessionId;
|
||||
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + userPhoneNumber;
|
||||
logger.info("Attempting to save entry to Redis for session {}. Type: {}", sessionId, newEntry.type().name());
|
||||
|
||||
return redisTemplate.opsForValue().get(sessionKey)
|
||||
.defaultIfEmpty(ConversationSessionDTO.create(sessionId, userId, userPhoneNumber))
|
||||
.flatMap(session -> {
|
||||
ConversationSessionDTO sessionWithUpdatedTelefono = session.withTelefono(userPhoneNumber);
|
||||
ConversationSessionDTO updatedSession = sessionWithUpdatedTelefono.withAddedEntry(newEntry);
|
||||
|
||||
logger.info("Attempting to set updated session {} with new entry type {} in Redis.", sessionId, newEntry.type().name());
|
||||
return redisTemplate.opsForValue().set(sessionKey, updatedSession, SESSION_TTL)
|
||||
.then(stringRedisTemplate.opsForValue().set(phoneToSessionKey, sessionId, SESSION_TTL));
|
||||
})
|
||||
.doOnSuccess(success -> logger.info("Successfully saved updated session and phone mapping to Redis for session {}. Entry Type: {}", sessionId, newEntry.type().name()))
|
||||
.doOnError(e -> logger.error("Error appending entry to Redis for session {}: {}", sessionId, e.getMessage(), e))
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<ConversationSessionDTO> getSessionByTelefono(String telefono) {
|
||||
if (telefono == null || telefono.trim().isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
String phoneToSessionKey = PHONE_TO_SESSION_KEY_PREFIX + telefono;
|
||||
logger.debug("Attempting to retrieve session ID for phone number {} from Redis.", telefono);
|
||||
|
||||
return stringRedisTemplate.opsForValue().get(phoneToSessionKey)
|
||||
.flatMap(sessionId -> {
|
||||
logger.debug("Found session ID {} for phone number {}. Retrieving session data.", sessionId, telefono);
|
||||
return redisTemplate.opsForValue().get(SESSION_KEY_PREFIX + sessionId);
|
||||
})
|
||||
.doOnSuccess(session -> {
|
||||
if (session != null) {
|
||||
logger.info("Successfully retrieved session {} by phone number {}.", session.sessionId(), telefono);
|
||||
} else {
|
||||
logger.info("No session found in Redis for phone number {}.", telefono);
|
||||
}
|
||||
})
|
||||
.doOnError(e -> logger.error("Error retrieving session by phone number {}: {}", telefono, e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// src/main/java/com/example/util/FirestoreTimestampDeserializer.java
|
||||
package com.example.util;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.google.cloud.Timestamp;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Custom Jackson Deserializer for com.google.cloud.Timestamp.
|
||||
* Handles deserialization from embedded objects (direct Timestamp instances),
|
||||
* ISO 8601 strings, and JSON objects with "seconds" and "nanos" fields.
|
||||
*/
|
||||
public class FirestoreTimestampDeserializer extends JsonDeserializer<Timestamp> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FirestoreTimestampDeserializer.class);
|
||||
|
||||
@Override
|
||||
public Timestamp deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
JsonToken token = p.getCurrentToken();
|
||||
|
||||
if (token == JsonToken.VALUE_EMBEDDED_OBJECT) {
|
||||
// This is the ideal case when ObjectMapper.convertValue gets a direct Timestamp object
|
||||
Object embedded = p.getEmbeddedObject();
|
||||
if (embedded instanceof Timestamp) {
|
||||
logger.debug("FirestoreTimestampDeserializer: Deserializing from embedded Timestamp object: {}", embedded);
|
||||
return (Timestamp) embedded;
|
||||
}
|
||||
} else if (token == JsonToken.VALUE_STRING) {
|
||||
// Handles cases where the timestamp is represented as an ISO 8601 string
|
||||
String timestampString = p.getText();
|
||||
try {
|
||||
logger.debug("FirestoreTimestampDeserializer: Deserializing from String: {}", timestampString);
|
||||
return Timestamp.parseTimestamp(timestampString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("FirestoreTimestampDeserializer: Failed to parse timestamp string: '{}'", timestampString, e);
|
||||
throw new IOException("Failed to parse timestamp string: " + timestampString, e);
|
||||
}
|
||||
} else if (token == JsonToken.START_OBJECT) {
|
||||
// This is crucial for handling the "Cannot deserialize ... from Object value (token JsonToken.START_OBJECT)" error.
|
||||
// It assumes the object represents { "seconds": X, "nanos": Y }
|
||||
logger.debug("FirestoreTimestampDeserializer: Deserializing from JSON object.");
|
||||
|
||||
// Suppress the unchecked warning here, as we expect a Map<String, Number>
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Number> map = p.readValueAs(Map.class);
|
||||
|
||||
if (map != null && map.containsKey("seconds") && map.containsKey("nanos")) {
|
||||
Number secondsNum = map.get("seconds");
|
||||
Number nanosNum = map.get("nanos");
|
||||
|
||||
if (secondsNum != null && nanosNum != null) {
|
||||
Long seconds = secondsNum.longValue();
|
||||
Integer nanos = nanosNum.intValue();
|
||||
return Timestamp.ofTimeSecondsAndNanos(seconds, nanos);
|
||||
}
|
||||
}
|
||||
logger.error("FirestoreTimestampDeserializer: JSON object missing 'seconds' or 'nanos' fields, or fields are not numbers.");
|
||||
}
|
||||
|
||||
// If none of the above formats match, log an error and delegate to default handling
|
||||
logger.error("FirestoreTimestampDeserializer: Unexpected token type for Timestamp deserialization. Expected Embedded Object, String, or START_OBJECT. Got: {}", token);
|
||||
// This will likely re-throw an error indicating inability to deserialize.
|
||||
return (Timestamp) ctxt.handleUnexpectedToken(Timestamp.class, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// src/main/java/com/example/util/FirestoreTimestampSerializer.java
|
||||
package com.example.util;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.google.cloud.Timestamp;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Custom Jackson Serializer for com.google.cloud.Timestamp.
|
||||
* This is crucial for ObjectMapper.convertValue to correctly handle Timestamp objects
|
||||
* when they are encountered in a Map<String, Object> and need to be internally
|
||||
* serialized before deserialization into a DTO. It converts Timestamp into a
|
||||
* simple JSON object with "seconds" and "nanos" fields.
|
||||
*/
|
||||
public class FirestoreTimestampSerializer extends JsonSerializer<Timestamp> {
|
||||
|
||||
@Override
|
||||
public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
} else {
|
||||
// Write Timestamp as a JSON object with seconds and nanos
|
||||
gen.writeStartObject();
|
||||
gen.writeNumberField("seconds", value.getSeconds());
|
||||
gen.writeNumberField("nanos", value.getNanos());
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/example/util/ProtobufUtil.java
Normal file
81
src/main/java/com/example/util/ProtobufUtil.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.example.util;
|
||||
|
||||
import com.google.protobuf.ListValue;
|
||||
import com.google.protobuf.NullValue;
|
||||
import com.google.protobuf.Struct;
|
||||
import com.google.protobuf.Value;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ProtobufUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProtobufUtil.class);
|
||||
|
||||
/**
|
||||
* Converts a Java Object to a Protobuf Value.
|
||||
* Supports primitive types, String, Map, and List.
|
||||
* Maps will be converted to Protobuf Structs.
|
||||
* Lists will be converted to Protobuf ListValues.
|
||||
*/
|
||||
public static Value convertJavaObjectToProtobufValue(Object obj) {
|
||||
if (obj == null) {
|
||||
return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
|
||||
} else if (obj instanceof Boolean) {
|
||||
return Value.newBuilder().setBoolValue((Boolean) obj).build();
|
||||
} else if (obj instanceof Integer) {
|
||||
return Value.newBuilder().setNumberValue(((Integer) obj).doubleValue()).build();
|
||||
} else if (obj instanceof Long) {
|
||||
return Value.newBuilder().setNumberValue(((Long) obj).doubleValue()).build();
|
||||
} else if (obj instanceof Double) {
|
||||
return Value.newBuilder().setNumberValue((Double) obj).build();
|
||||
} else if (obj instanceof String) {
|
||||
return Value.newBuilder().setStringValue((String) obj).build();
|
||||
} else if (obj instanceof Map) {
|
||||
Struct.Builder structBuilder = Struct.newBuilder();
|
||||
((Map<?, ?>) obj).forEach((key, val) ->
|
||||
structBuilder.putFields(String.valueOf(key), convertJavaObjectToProtobufValue(val))
|
||||
);
|
||||
return Value.newBuilder().setStructValue(structBuilder.build()).build();
|
||||
} else if (obj instanceof List) {
|
||||
ListValue.Builder listValueBuilder = ListValue.newBuilder();
|
||||
((List<?>) obj).forEach(item ->
|
||||
listValueBuilder.addValues(convertJavaObjectToProtobufValue(item))
|
||||
);
|
||||
return Value.newBuilder().setListValue(listValueBuilder.build()).build();
|
||||
}
|
||||
logger.warn("Unsupported type for Protobuf conversion: {}. Converting to String.", obj.getClass().getName());
|
||||
return Value.newBuilder().setStringValue(obj.toString()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Protobuf Value to a Java Object.
|
||||
* Supports Null, Boolean, Number, String, Struct (to Map), and ListValue (to List).
|
||||
*/
|
||||
public static Object convertProtobufValueToJavaObject(Value protobufValue) {
|
||||
return switch (protobufValue.getKindCase()) {
|
||||
case NULL_VALUE -> null;
|
||||
case BOOL_VALUE -> protobufValue.getBoolValue();
|
||||
case NUMBER_VALUE -> protobufValue.getNumberValue();
|
||||
case STRING_VALUE -> protobufValue.getStringValue();
|
||||
case STRUCT_VALUE -> protobufValue.getStructValue().getFieldsMap().entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> convertProtobufValueToJavaObject(entry.getValue()),
|
||||
(oldValue, newValue) -> oldValue,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
case LIST_VALUE -> protobufValue.getListValue().getValuesList().stream()
|
||||
.map(ProtobufUtil::convertProtobufValueToJavaObject) // Use static method reference
|
||||
.collect(Collectors.toList());
|
||||
default -> {
|
||||
logger.warn("Unsupported Protobuf Value type: {}. Returning null.", protobufValue.getKindCase());
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
85
src/main/resources/application.properties
Normal file
85
src/main/resources/application.properties
Normal file
@@ -0,0 +1,85 @@
|
||||
# Firestore Configuration Properties
|
||||
# --------------------------------
|
||||
|
||||
# Project ID Configuration
|
||||
# Use this setting if you want to manually specify a GCP Project instead of inferring
|
||||
# from your machine's environment.
|
||||
spring.cloud.gcp.firestore.project-id=app-jovenes
|
||||
|
||||
# Credentials Configuration
|
||||
# Use this setting if you want to manually specify service account credentials instead of inferring
|
||||
# from the machine's environment for firestore.
|
||||
#spring.cloud.gcp.firestore.credentials.location=file:{PATH_TO_YOUR_CREDENTIALS_FILE}
|
||||
|
||||
# Firestore Emulator Configuration (for local development)
|
||||
#spring.cloud.gcp.firestore.emulator-host=localhost:8080
|
||||
spring.cloud.gcp.firestore.emulator.enabled=false
|
||||
|
||||
# Firestore Database Configuration
|
||||
# ------------------------------------------
|
||||
spring.cloud.gcp.firestore.database-id=app-jovenes-cache-database
|
||||
spring.cloud.gcp.firestore.host=firestore.googleapis.com
|
||||
spring.cloud.gcp.firestore.port=443
|
||||
|
||||
# Memorystore (Redis) Configuration Properties
|
||||
# ------------------------------------------
|
||||
|
||||
# Basic Connection Settings
|
||||
#Secret Manager recomendation for credentials
|
||||
spring.data.redis.host=10.241.0.11
|
||||
spring.data.redis.port=6379
|
||||
#spring.data.redis.password=23cb4c76-9d96-4c74-b8c0-778fb364877a
|
||||
#spring.data.redis.username=default
|
||||
|
||||
# Connection Pool Settings
|
||||
# spring.data.redis.lettuce.pool.max-active=8
|
||||
# spring.data.redis.lettuce.pool.max-idle=8
|
||||
# spring.data.redis.lettuce.pool.min-idle=0
|
||||
# spring.data.redis.lettuce.pool.max-wait=-1ms
|
||||
|
||||
# SSL Configuration (if using SSL)
|
||||
# spring.data.redis.ssl=true
|
||||
# spring.data.redis.ssl.key-store=classpath:keystore.p12
|
||||
# spring.data.redis.ssl.key-store-password=your-keystore-password
|
||||
|
||||
# Timeout Settings
|
||||
# spring.data.redis.timeout=2000ms
|
||||
# spring.data.redis.lettuce.shutdown-timeout=100ms
|
||||
|
||||
# Cluster Configuration (if using Redis Cluster)
|
||||
# spring.data.redis.cluster.nodes=localhost:6379,localhost:6380,localhost:6381
|
||||
# spring.data.redis.cluster.max-redirects=3
|
||||
|
||||
# Sentinel Configuration (if using Redis Sentinel)
|
||||
# spring.data.redis.sentinel.master=mymaster
|
||||
# spring.data.redis.sentinel.nodes=localhost:26379,localhost:26380,localhost:26381
|
||||
|
||||
# Additional Redis Settings
|
||||
# spring.data.redis.database=0
|
||||
# spring.data.redis.client-type=lettuce
|
||||
# spring.data.redis.lettuce.cluster.refresh.period=1000ms
|
||||
|
||||
# Google Cloud StorageConfiguration
|
||||
# ------------------------------------------
|
||||
gcs.bucket.name=app-jovenes-bucket
|
||||
spring.cloud.gcp.project-id=app-jovenes
|
||||
|
||||
# Dialogflow CX Configuration
|
||||
# ------------------------------------------
|
||||
dialogflow.cx.project-id=app-jovenes
|
||||
dialogflow.cx.location=us-central1
|
||||
dialogflow.cx.agent-id=3b9f2354-8556-4363-9e70-fa8283582a3e
|
||||
dialogflow.default-language-code=es
|
||||
|
||||
# =========================================================
|
||||
# Google Generative AI (Gemini) Configuration
|
||||
# =========================================================
|
||||
# Your Google Cloud Project ID where the Vertex AI service is enabled.
|
||||
google.cloud.project=app-jovenes
|
||||
|
||||
# The Google Cloud region where you want to access the Gemini model.
|
||||
# Common regions: us-central1, europe-west1, asia-northeast1 etc.
|
||||
google.cloud.location=us-central1
|
||||
|
||||
# The name of the Gemini model to use for summarization.
|
||||
gemini.model.name=gemini-2.0-flash-001
|
||||
57
src/main/resources/static/index.html
Normal file
57
src/main/resources/static/index.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Spring Data Firestore Sample</title>
|
||||
</head>
|
||||
|
||||
<style>
|
||||
html * {
|
||||
font-family: Roboto, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 50em;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Spring Data Firestore Sample</h1>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h2>Firestore Control Panel</h2>
|
||||
|
||||
<p>
|
||||
This section allows you to read User entities in Firestore.
|
||||
Some values are prefilled as an example of what you can type in.
|
||||
</p>
|
||||
|
||||
<div class="panel">
|
||||
<a href="/users">Show all users</a>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<b>Show all users with age</b>
|
||||
<form action="/users/age" method="get">
|
||||
Age: <input type="text" name="age" value="30">
|
||||
<input type="submit" value="submit">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<a href="https://console.cloud.google.com/firestore/data">View your Firestore data in the Cloud Console</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
src/test/resources/application-test.properties
Normal file
1
src/test/resources/application-test.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring.cloud.gcp.firestore.database-id=firestoredb
|
||||
14
src/test/resources/logback-test.xml
Normal file
14
src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user