Populated through Spring Boot constructor binding; construct directly with the canonical
+ * constructor in tests. The {@code supportedFormats} list is defensively copied on both
+ * construction and access so the configuration cannot be mutated through a shared reference.
*/
@ConfigurationProperties(prefix = "ingestion")
@Validated
-public class IngestionConfig {
-
+public record IngestionConfig(
@NotNull(message = "Chunk size must be specified")
- @Positive(message = "Chunk size must be positive")
- private Integer chunkSize;
-
+ @Positive(message = "Chunk size must be positive")
+ Integer chunkSize,
@NotNull(message = "Chunk overlap must be specified")
- @PositiveOrZero(message = "Chunk overlap must be zero or positive")
- private Integer chunkOverlap;
-
+ @PositiveOrZero(message = "Chunk overlap must be zero or positive")
+ Integer chunkOverlap,
@NotNull(message = "Batch size must be specified")
- @Positive(message = "Batch size must be positive")
- private Integer batchSize;
-
- private List Populated through Spring Boot constructor binding; construct directly with the canonical
+ * constructor in tests.
*/
@ConfigurationProperties(prefix = "model")
@Validated
-public class ModelConfig {
-
- @NotBlank(message = "Model provider type must be specified")
- private String provider;
-
- @Valid
- private SelfHostedSettings selfHosted;
-
- @Valid
- private OpenAISettings openai;
-
- @Valid
- private AnthropicSettings anthropic;
-
- @Valid
- private GeminiSettings gemini;
-
- @NotNull(message = "Temperature must be specified")
- private Double temperature;
-
+public record ModelConfig(
+ @NotBlank(message = "Model provider type must be specified") String provider,
+ @Valid SelfHostedSettings selfHosted,
+ @Valid OpenAISettings openai,
+ @Valid AnthropicSettings anthropic,
+ @Valid GeminiSettings gemini,
+ @NotNull(message = "Temperature must be specified") Double temperature,
@NotNull(message = "Max tokens must be specified")
- @Positive(message = "Max tokens must be positive")
- private Integer maxTokens;
-
- // Getters and Setters
-
- public String getProvider() {
- return provider;
- }
-
- public void setProvider(String provider) {
- this.provider = provider;
- }
-
- public SelfHostedSettings getSelfHosted() {
- return selfHosted;
- }
-
- public void setSelfHosted(SelfHostedSettings selfHosted) {
- this.selfHosted = selfHosted;
- }
-
- public OpenAISettings getOpenai() {
- return openai;
- }
-
- public void setOpenai(OpenAISettings openai) {
- this.openai = openai;
- }
-
- public AnthropicSettings getAnthropic() {
- return anthropic;
- }
-
- public void setAnthropic(AnthropicSettings anthropic) {
- this.anthropic = anthropic;
- }
-
- public GeminiSettings getGemini() {
- return gemini;
- }
-
- public void setGemini(GeminiSettings gemini) {
- this.gemini = gemini;
- }
-
- public Double getTemperature() {
- return temperature;
- }
-
- public void setTemperature(Double temperature) {
- this.temperature = temperature;
- }
-
- public Integer getMaxTokens() {
- return maxTokens;
- }
-
- public void setMaxTokens(Integer maxTokens) {
- this.maxTokens = maxTokens;
- }
-
- /**
- * Configuration for self-hosted models (e.g., Ollama).
- */
- public static class SelfHostedSettings {
- @NotBlank(message = "Self-hosted base URL must be specified")
- private String baseUrl;
-
- @NotBlank(message = "Self-hosted model name must be specified")
- private String modelName;
-
- @Positive(message = "Timeout seconds must be positive")
- private Integer timeoutSeconds;
-
- public String getBaseUrl() {
- return baseUrl;
- }
-
- public void setBaseUrl(String baseUrl) {
- this.baseUrl = baseUrl;
- }
-
- public String getModelName() {
- return modelName;
- }
-
- public void setModelName(String modelName) {
- this.modelName = modelName;
- }
-
- public Integer getTimeoutSeconds() {
- return timeoutSeconds;
- }
-
- public void setTimeoutSeconds(Integer timeoutSeconds) {
- this.timeoutSeconds = timeoutSeconds;
- }
- }
-
- /**
- * Configuration for OpenAI API.
- */
- public static class OpenAISettings {
- private String apiKey;
-
- @NotBlank(message = "OpenAI model name must be specified")
- private String modelName;
-
- private String baseUrl;
-
- @Positive(message = "Timeout seconds must be positive")
- private Integer timeoutSeconds;
-
- public String getApiKey() {
- return apiKey;
- }
-
- public void setApiKey(String apiKey) {
- this.apiKey = apiKey;
- }
-
- public String getModelName() {
- return modelName;
- }
-
- public void setModelName(String modelName) {
- this.modelName = modelName;
- }
-
- public String getBaseUrl() {
- return baseUrl;
- }
-
- public void setBaseUrl(String baseUrl) {
- this.baseUrl = baseUrl;
- }
-
- public Integer getTimeoutSeconds() {
- return timeoutSeconds;
- }
-
- public void setTimeoutSeconds(Integer timeoutSeconds) {
- this.timeoutSeconds = timeoutSeconds;
- }
- }
-
- /**
- * Configuration for Anthropic API.
- */
- public static class AnthropicSettings {
- private String apiKey;
-
- @NotBlank(message = "Anthropic model name must be specified")
- private String modelName;
-
- @Positive(message = "Timeout seconds must be positive")
- private Integer timeoutSeconds;
-
- public String getApiKey() {
- return apiKey;
- }
-
- public void setApiKey(String apiKey) {
- this.apiKey = apiKey;
- }
-
- public String getModelName() {
- return modelName;
- }
-
- public void setModelName(String modelName) {
- this.modelName = modelName;
- }
-
- public Integer getTimeoutSeconds() {
- return timeoutSeconds;
- }
-
- public void setTimeoutSeconds(Integer timeoutSeconds) {
- this.timeoutSeconds = timeoutSeconds;
- }
- }
-
- /**
- * Configuration for Google Gemini API via Vertex AI.
- */
- public static class GeminiSettings {
- private String projectId;
-
- @NotBlank(message = "Gemini location must be specified")
- private String location;
-
- @NotBlank(message = "Gemini model name must be specified")
- private String modelName;
-
- private String apiKey;
-
- @Positive(message = "Timeout seconds must be positive")
- private Integer timeoutSeconds;
-
- public String getProjectId() {
- return projectId;
- }
-
- public void setProjectId(String projectId) {
- this.projectId = projectId;
- }
-
- public String getLocation() {
- return location;
- }
-
- public void setLocation(String location) {
- this.location = location;
- }
-
- public String getModelName() {
- return modelName;
- }
-
- public void setModelName(String modelName) {
- this.modelName = modelName;
- }
-
- public String getApiKey() {
- return apiKey;
- }
-
- public void setApiKey(String apiKey) {
- this.apiKey = apiKey;
- }
-
- public Integer getTimeoutSeconds() {
- return timeoutSeconds;
- }
-
- public void setTimeoutSeconds(Integer timeoutSeconds) {
- this.timeoutSeconds = timeoutSeconds;
- }
- }
+ @Positive(message = "Max tokens must be positive")
+ Integer maxTokens) {
+
+ /** Configuration for self-hosted models (e.g., Ollama). */
+ public record SelfHostedSettings(
+ @NotBlank(message = "Self-hosted base URL must be specified") String baseUrl,
+ @NotBlank(message = "Self-hosted model name must be specified") String modelName,
+ @Positive(message = "Timeout seconds must be positive") Integer timeoutSeconds) {}
+
+ /** Configuration for OpenAI API. */
+ public record OpenAISettings(
+ String apiKey,
+ @NotBlank(message = "OpenAI model name must be specified") String modelName,
+ String baseUrl,
+ @Positive(message = "Timeout seconds must be positive") Integer timeoutSeconds) {}
+
+ /** Configuration for Anthropic API. */
+ public record AnthropicSettings(
+ String apiKey,
+ @NotBlank(message = "Anthropic model name must be specified") String modelName,
+ @Positive(message = "Timeout seconds must be positive") Integer timeoutSeconds) {}
+
+ /** Configuration for Google Gemini API via Vertex AI. */
+ public record GeminiSettings(
+ String projectId,
+ @NotBlank(message = "Gemini location must be specified") String location,
+ @NotBlank(message = "Gemini model name must be specified") String modelName,
+ String apiKey,
+ @Positive(message = "Timeout seconds must be positive") Integer timeoutSeconds) {}
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryConfig.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryConfig.java
index 8c49cb5..0138615 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryConfig.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryConfig.java
@@ -1,5 +1,7 @@
package br.com.arquivolivre.myjavagenie.config;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
+import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
@@ -8,6 +10,7 @@
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
@@ -18,350 +21,226 @@
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.semconv.ResourceAttributes;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.stereotype.Component;
-
-import java.time.Duration;
-import java.util.concurrent.TimeUnit;
/**
- * Configuration class for OpenTelemetry observability.
- * Sets up traces, metrics, and logs exporters with OTLP protocol.
+ * Configuration class for OpenTelemetry observability. Sets up traces, metrics, and logs exporters
+ * with OTLP protocol.
*/
@Configuration
@ConditionalOnProperty(name = "opentelemetry.enabled", havingValue = "true", matchIfMissing = false)
+@EnableConfigurationProperties(OpenTelemetryConfig.OpenTelemetryProperties.class)
public class OpenTelemetryConfig {
- private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryConfig.class);
-
- private final OpenTelemetryProperties properties;
-
- public OpenTelemetryConfig(OpenTelemetryProperties properties) {
- this.properties = properties;
- logger.info("Initializing OpenTelemetry with service name: {}", properties.getServiceName());
- }
-
- /**
- * Creates the OpenTelemetry SDK instance with configured exporters.
- */
- @Bean
- public OpenTelemetry openTelemetry() {
- Resource resource = Resource.getDefault()
- .merge(Resource.create(Attributes.builder()
- .put(ResourceAttributes.SERVICE_NAME, properties.getServiceName())
- .put(ResourceAttributes.SERVICE_VERSION, properties.getServiceVersion())
- .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, properties.getEnvironment())
+ private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryConfig.class);
+
+ private final OpenTelemetryProperties properties;
+
+ public OpenTelemetryConfig(OpenTelemetryProperties properties) {
+ this.properties = properties;
+ logger.info(
+ "Initializing OpenTelemetry with service name: {}",
+ LogSanitizer.sanitize(properties.serviceName()));
+ }
+
+ /** Creates the OpenTelemetry SDK instance with configured exporters. */
+ @Bean
+ public OpenTelemetry openTelemetry() {
+ Resource resource =
+ Resource.getDefault()
+ .merge(
+ Resource.create(
+ Attributes.builder()
+ .put(ResourceAttributes.SERVICE_NAME, properties.serviceName())
+ .put(ResourceAttributes.SERVICE_VERSION, properties.serviceVersion())
+ .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, properties.environment())
.build()));
- var sdkBuilder = OpenTelemetrySdk.builder();
-
- // Configure Tracer Provider
- if (properties.getTraces().isEnabled()) {
- SdkTracerProvider tracerProvider = configurTracerProvider(resource);
- sdkBuilder.setTracerProvider(tracerProvider);
- logger.info("OpenTelemetry traces enabled with endpoint: {}",
- properties.getTraces().getEndpoint());
- }
-
- // Configure Meter Provider
- if (properties.getMetrics().isEnabled()) {
- SdkMeterProvider meterProvider = configureMeterProvider(resource);
- sdkBuilder.setMeterProvider(meterProvider);
- logger.info("OpenTelemetry metrics enabled with endpoint: {}",
- properties.getMetrics().getEndpoint());
- }
-
- // Configure Logger Provider
- if (properties.getLogs().isEnabled()) {
- SdkLoggerProvider loggerProvider = configureLoggerProvider(resource);
- sdkBuilder.setLoggerProvider(loggerProvider);
- logger.info("OpenTelemetry logs enabled with endpoint: {}",
- properties.getLogs().getEndpoint());
- }
+ var sdkBuilder = OpenTelemetrySdk.builder();
- OpenTelemetry openTelemetry = sdkBuilder
- .setPropagators(ContextPropagators.noop())
- .buildAndRegisterGlobal();
-
- logger.info("OpenTelemetry SDK initialized successfully");
- return openTelemetry;
+ // Configure Tracer Provider
+ if (properties.traces().enabled()) {
+ SdkTracerProvider tracerProvider = configurTracerProvider(resource);
+ sdkBuilder.setTracerProvider(tracerProvider);
+ logger.info(
+ "OpenTelemetry traces enabled with endpoint: {}",
+ LogSanitizer.sanitize(properties.traces().endpoint()));
}
- /**
- * Configures the tracer provider with OTLP exporter.
- */
- private SdkTracerProvider configurTracerProvider(Resource resource) {
- try {
- OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
- .setEndpoint(properties.getTraces().getEndpoint())
- .setTimeout(10, TimeUnit.SECONDS)
- .build();
-
- return SdkTracerProvider.builder()
- .setResource(resource)
- .addSpanProcessor(BatchSpanProcessor.builder(spanExporter)
- .setScheduleDelay(Duration.ofSeconds(5))
- .build())
- .setSampler(Sampler.traceIdRatioBased(properties.getTraces().getSamplingRate()))
- .build();
- } catch (Exception e) {
- logger.error("Failed to configure tracer provider, traces will not be exported: {}", e.getMessage());
- // Return a no-op tracer provider to allow application to continue
- return SdkTracerProvider.builder()
- .setResource(resource)
- .setSampler(Sampler.alwaysOff())
- .build();
- }
+ // Configure Meter Provider
+ if (properties.metrics().enabled()) {
+ SdkMeterProvider meterProvider = configureMeterProvider(resource);
+ sdkBuilder.setMeterProvider(meterProvider);
+ logger.info(
+ "OpenTelemetry metrics enabled with endpoint: {}",
+ LogSanitizer.sanitize(properties.metrics().endpoint()));
}
- /**
- * Configures the meter provider with OTLP exporter.
- */
- private SdkMeterProvider configureMeterProvider(Resource resource) {
- try {
- OtlpGrpcMetricExporter metricExporter = OtlpGrpcMetricExporter.builder()
- .setEndpoint(properties.getMetrics().getEndpoint())
- .setTimeout(10, TimeUnit.SECONDS)
- .build();
-
- return SdkMeterProvider.builder()
- .setResource(resource)
- .registerMetricReader(PeriodicMetricReader.builder(metricExporter)
- .setInterval(Duration.ofMillis(properties.getMetrics().getExportIntervalMillis()))
- .build())
- .build();
- } catch (Exception e) {
- logger.error("Failed to configure meter provider, metrics will not be exported: {}", e.getMessage());
- // Return a no-op meter provider to allow application to continue
- return SdkMeterProvider.builder()
- .setResource(resource)
- .build();
- }
+ // Configure Logger Provider
+ if (properties.logs().enabled()) {
+ SdkLoggerProvider loggerProvider = configureLoggerProvider(resource);
+ sdkBuilder.setLoggerProvider(loggerProvider);
+ logger.info(
+ "OpenTelemetry logs enabled with endpoint: {}",
+ LogSanitizer.sanitize(properties.logs().endpoint()));
}
- /**
- * Configures the logger provider with OTLP exporter.
- */
- private SdkLoggerProvider configureLoggerProvider(Resource resource) {
- try {
- OtlpGrpcLogRecordExporter logExporter = OtlpGrpcLogRecordExporter.builder()
- .setEndpoint(properties.getLogs().getEndpoint())
- .setTimeout(10, TimeUnit.SECONDS)
- .build();
-
- return SdkLoggerProvider.builder()
- .setResource(resource)
- .addLogRecordProcessor(BatchLogRecordProcessor.builder(logExporter)
- .setScheduleDelay(Duration.ofSeconds(5))
- .build())
- .build();
- } catch (Exception e) {
- logger.error("Failed to configure logger provider, logs will not be exported: {}", e.getMessage());
- // Return a no-op logger provider to allow application to continue
- return SdkLoggerProvider.builder()
- .setResource(resource)
- .build();
- }
+ // Build a local SDK bean. Register as global only when nothing else (e.g. the
+ // OpenTelemetry Spring starter) has already called GlobalOpenTelemetry.set —
+ // otherwise Spring tests fail with "GlobalOpenTelemetry.set has already been called".
+ OpenTelemetrySdk openTelemetry = sdkBuilder.setPropagators(ContextPropagators.noop()).build();
+ try {
+ GlobalOpenTelemetry.set(openTelemetry);
+ } catch (IllegalStateException alreadyRegistered) {
+ logger.warn(
+ "Global OpenTelemetry already registered; using SDK as Spring bean only: {}",
+ LogSanitizer.sanitize(alreadyRegistered.getMessage()));
}
- /**
- * Creates a Tracer bean for manual instrumentation.
- */
- @Bean
- public Tracer tracer(OpenTelemetry openTelemetry) {
- return openTelemetry.getTracer(properties.getServiceName());
+ // Bridge Logback log events into the OTel LoggerProvider so application logs are exported
+ // over OTLP. The appender is declared in logback-spring.xml and buffers events until this
+ // install call wires it to the freshly built SDK.
+ if (properties.logs().enabled()) {
+ OpenTelemetryAppender.install(openTelemetry);
+ logger.info("OpenTelemetry Logback appender installed for log export");
}
- /**
- * Creates a Meter bean for custom metrics.
- */
- @Bean
- public Meter meter(OpenTelemetry openTelemetry) {
- return openTelemetry.getMeter(properties.getServiceName());
+ logger.info("OpenTelemetry SDK initialized successfully");
+ return openTelemetry;
+ }
+
+ /** Configures the tracer provider with OTLP exporter. */
+ private SdkTracerProvider configurTracerProvider(Resource resource) {
+ try {
+ OtlpGrpcSpanExporter spanExporter =
+ OtlpGrpcSpanExporter.builder()
+ .setEndpoint(properties.traces().endpoint())
+ .setTimeout(10, TimeUnit.SECONDS)
+ .build();
+
+ return SdkTracerProvider.builder()
+ .setResource(resource)
+ .addSpanProcessor(
+ BatchSpanProcessor.builder(spanExporter)
+ .setScheduleDelay(Duration.ofSeconds(5))
+ .build())
+ .setSampler(Sampler.traceIdRatioBased(properties.traces().samplingRate()))
+ .build();
+ } catch (Exception e) {
+ logger.error(
+ "Failed to configure tracer provider, traces will not be exported: {}",
+ LogSanitizer.sanitize(e.getMessage()));
+ // Return a no-op tracer provider to allow application to continue
+ return SdkTracerProvider.builder()
+ .setResource(resource)
+ .setSampler(Sampler.alwaysOff())
+ .build();
}
-
- /**
- * Configuration properties for OpenTelemetry.
- */
- @Component
- @ConfigurationProperties(prefix = "opentelemetry")
- public static class OpenTelemetryProperties {
- private boolean enabled = true;
- private String serviceName = "java-rag-system";
- private String serviceVersion = "1.0.0";
- private String environment = "development";
- private TracesConfig traces = new TracesConfig();
- private MetricsConfig metrics = new MetricsConfig();
- private LogsConfig logs = new LogsConfig();
-
- // Getters and setters
- public boolean isEnabled() {
- return enabled;
- }
-
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
-
- public String getServiceName() {
- return serviceName;
- }
-
- public void setServiceName(String serviceName) {
- this.serviceName = serviceName;
- }
-
- public String getServiceVersion() {
- return serviceVersion;
- }
-
- public void setServiceVersion(String serviceVersion) {
- this.serviceVersion = serviceVersion;
- }
-
- public String getEnvironment() {
- return environment;
- }
-
- public void setEnvironment(String environment) {
- this.environment = environment;
- }
-
- public TracesConfig getTraces() {
- return traces;
- }
-
- public void setTraces(TracesConfig traces) {
- this.traces = traces;
- }
-
- public MetricsConfig getMetrics() {
- return metrics;
- }
-
- public void setMetrics(MetricsConfig metrics) {
- this.metrics = metrics;
- }
-
- public LogsConfig getLogs() {
- return logs;
- }
-
- public void setLogs(LogsConfig logs) {
- this.logs = logs;
- }
-
- public static class TracesConfig {
- private boolean enabled = true;
- private String exporter = "otlp";
- private String endpoint = "http://localhost:4317";
- private double samplingRate = 1.0;
-
- public boolean isEnabled() {
- return enabled;
- }
-
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
-
- public String getExporter() {
- return exporter;
- }
-
- public void setExporter(String exporter) {
- this.exporter = exporter;
- }
-
- public String getEndpoint() {
- return endpoint;
- }
-
- public void setEndpoint(String endpoint) {
- this.endpoint = endpoint;
- }
-
- public double getSamplingRate() {
- return samplingRate;
- }
-
- public void setSamplingRate(double samplingRate) {
- this.samplingRate = samplingRate;
- }
- }
-
- public static class MetricsConfig {
- private boolean enabled = true;
- private String exporter = "otlp";
- private String endpoint = "http://localhost:4317";
- private long exportIntervalMillis = 60000;
-
- public boolean isEnabled() {
- return enabled;
- }
-
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
-
- public String getExporter() {
- return exporter;
- }
-
- public void setExporter(String exporter) {
- this.exporter = exporter;
- }
-
- public String getEndpoint() {
- return endpoint;
- }
-
- public void setEndpoint(String endpoint) {
- this.endpoint = endpoint;
- }
-
- public long getExportIntervalMillis() {
- return exportIntervalMillis;
- }
-
- public void setExportIntervalMillis(long exportIntervalMillis) {
- this.exportIntervalMillis = exportIntervalMillis;
- }
- }
-
- public static class LogsConfig {
- private boolean enabled = true;
- private String exporter = "otlp";
- private String endpoint = "http://localhost:4317";
-
- public boolean isEnabled() {
- return enabled;
- }
-
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
-
- public String getExporter() {
- return exporter;
- }
-
- public void setExporter(String exporter) {
- this.exporter = exporter;
- }
-
- public String getEndpoint() {
- return endpoint;
- }
-
- public void setEndpoint(String endpoint) {
- this.endpoint = endpoint;
- }
- }
+ }
+
+ /** Configures the meter provider with OTLP exporter. */
+ private SdkMeterProvider configureMeterProvider(Resource resource) {
+ try {
+ OtlpGrpcMetricExporter metricExporter =
+ OtlpGrpcMetricExporter.builder()
+ .setEndpoint(properties.metrics().endpoint())
+ .setTimeout(10, TimeUnit.SECONDS)
+ .build();
+
+ return SdkMeterProvider.builder()
+ .setResource(resource)
+ .registerMetricReader(
+ PeriodicMetricReader.builder(metricExporter)
+ .setInterval(Duration.ofMillis(properties.metrics().exportIntervalMillis()))
+ .build())
+ .build();
+ } catch (Exception e) {
+ logger.error(
+ "Failed to configure meter provider, metrics will not be exported: {}",
+ LogSanitizer.sanitize(e.getMessage()));
+ // Return a no-op meter provider to allow application to continue
+ return SdkMeterProvider.builder().setResource(resource).build();
+ }
+ }
+
+ /** Configures the logger provider with OTLP exporter. */
+ private SdkLoggerProvider configureLoggerProvider(Resource resource) {
+ try {
+ OtlpGrpcLogRecordExporter logExporter =
+ OtlpGrpcLogRecordExporter.builder()
+ .setEndpoint(properties.logs().endpoint())
+ .setTimeout(10, TimeUnit.SECONDS)
+ .build();
+
+ return SdkLoggerProvider.builder()
+ .setResource(resource)
+ .addLogRecordProcessor(
+ BatchLogRecordProcessor.builder(logExporter)
+ .setScheduleDelay(Duration.ofSeconds(5))
+ .build())
+ .build();
+ } catch (Exception e) {
+ logger.error(
+ "Failed to configure logger provider, logs will not be exported: {}",
+ LogSanitizer.sanitize(e.getMessage()));
+ // Return a no-op logger provider to allow application to continue
+ return SdkLoggerProvider.builder().setResource(resource).build();
}
+ }
+
+ /** Creates a Tracer bean for manual instrumentation. */
+ @Bean
+ public Tracer tracer(OpenTelemetry openTelemetry) {
+ return openTelemetry.getTracer(properties.serviceName());
+ }
+
+ /** Creates a Meter bean for custom metrics. */
+ @Bean
+ public Meter meter(OpenTelemetry openTelemetry) {
+ return openTelemetry.getMeter(properties.serviceName());
+ }
+
+ /**
+ * Immutable configuration properties for OpenTelemetry. Populated through Spring Boot constructor
+ * binding, falling back to the declared defaults when properties are absent.
+ */
+ @ConfigurationProperties(prefix = "opentelemetry")
+ public record OpenTelemetryProperties(
+ @DefaultValue("true") boolean enabled,
+ @DefaultValue("java-rag-system") String serviceName,
+ @DefaultValue("1.0.0") String serviceVersion,
+ @DefaultValue("development") String environment,
+ @DefaultValue TracesConfig traces,
+ @DefaultValue MetricsConfig metrics,
+ @DefaultValue LogsConfig logs) {
+
+ /** Trace exporter configuration. */
+ public record TracesConfig(
+ @DefaultValue("true") boolean enabled,
+ @DefaultValue("otlp") String exporter,
+ @DefaultValue("http://localhost:4317") String endpoint,
+ @DefaultValue("1.0") double samplingRate) {}
+
+ /** Metric exporter configuration. */
+ public record MetricsConfig(
+ @DefaultValue("true") boolean enabled,
+ @DefaultValue("otlp") String exporter,
+ @DefaultValue("http://localhost:4317") String endpoint,
+ @DefaultValue("60000") long exportIntervalMillis) {}
+
+ /** Log exporter configuration. */
+ public record LogsConfig(
+ @DefaultValue("true") boolean enabled,
+ @DefaultValue("otlp") String exporter,
+ @DefaultValue("http://localhost:4317") String endpoint) {}
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryHealthIndicator.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryHealthIndicator.java
index db54203..f6c3eed 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryHealthIndicator.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/OpenTelemetryHealthIndicator.java
@@ -1,6 +1,11 @@
package br.com.arquivolivre.myjavagenie.config;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
import io.opentelemetry.api.OpenTelemetry;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.channels.SocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.health.Health;
@@ -8,93 +13,89 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
-import java.io.IOException;
-import java.net.Socket;
-import java.net.URI;
-
/**
- * Health indicator for OpenTelemetry collector connectivity.
- * Checks if the OTLP endpoint is reachable.
+ * Health indicator for OpenTelemetry collector connectivity. Checks if the OTLP endpoint is
+ * reachable.
*/
@Component
@ConditionalOnProperty(name = "opentelemetry.enabled", havingValue = "true", matchIfMissing = false)
public class OpenTelemetryHealthIndicator implements HealthIndicator {
- private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryHealthIndicator.class);
+ private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryHealthIndicator.class);
- private final OpenTelemetryConfig.OpenTelemetryProperties properties;
- private final OpenTelemetry openTelemetry;
+ private final OpenTelemetryConfig.OpenTelemetryProperties properties;
+ private final OpenTelemetry openTelemetry;
- public OpenTelemetryHealthIndicator(OpenTelemetryConfig.OpenTelemetryProperties properties,
- OpenTelemetry openTelemetry) {
- this.properties = properties;
- this.openTelemetry = openTelemetry;
- }
+ public OpenTelemetryHealthIndicator(
+ OpenTelemetryConfig.OpenTelemetryProperties properties, OpenTelemetry openTelemetry) {
+ this.properties = properties;
+ this.openTelemetry = openTelemetry;
+ }
- @Override
- public Health health() {
- try {
- // Check if OpenTelemetry is initialized
- if (openTelemetry == null) {
- return Health.down()
- .withDetail("status", "OpenTelemetry not initialized")
- .build();
- }
+ @Override
+ public Health health() {
+ try {
+ // Check if OpenTelemetry is initialized
+ if (openTelemetry == null) {
+ return Health.down().withDetail("status", "OpenTelemetry not initialized").build();
+ }
- // Check collector connectivity
- boolean collectorReachable = checkCollectorConnectivity();
+ // Check collector connectivity
+ boolean collectorReachable = checkCollectorConnectivity();
- if (collectorReachable) {
- return Health.up()
- .withDetail("status", "OpenTelemetry collector is reachable")
- .withDetail("endpoint", properties.getTraces().getEndpoint())
- .withDetail("service", properties.getServiceName())
- .withDetail("traces_enabled", properties.getTraces().isEnabled())
- .withDetail("metrics_enabled", properties.getMetrics().isEnabled())
- .withDetail("logs_enabled", properties.getLogs().isEnabled())
- .build();
- } else {
- return Health.down()
- .withDetail("status", "OpenTelemetry collector is not reachable")
- .withDetail("endpoint", properties.getTraces().getEndpoint())
- .withDetail("note", "Application continues to function, but telemetry data may not be exported")
- .build();
- }
- } catch (Exception e) {
- logger.error("Error checking OpenTelemetry health", e);
- return Health.down()
- .withDetail("status", "Error checking OpenTelemetry health")
- .withDetail("error", e.getMessage())
- .build();
- }
+ if (collectorReachable) {
+ return Health.up()
+ .withDetail("status", "OpenTelemetry collector is reachable")
+ .withDetail("endpoint", properties.traces().endpoint())
+ .withDetail("service", properties.serviceName())
+ .withDetail("traces_enabled", properties.traces().enabled())
+ .withDetail("metrics_enabled", properties.metrics().enabled())
+ .withDetail("logs_enabled", properties.logs().enabled())
+ .build();
+ } else {
+ return Health.down()
+ .withDetail("status", "OpenTelemetry collector is not reachable")
+ .withDetail("endpoint", properties.traces().endpoint())
+ .withDetail(
+ "note", "Application continues to function, but telemetry data may not be exported")
+ .build();
+ }
+ } catch (Exception e) {
+ logger.error("Error checking OpenTelemetry health", e);
+ return Health.down()
+ .withDetail("status", "Error checking OpenTelemetry health")
+ .withDetail("error", e.getMessage())
+ .build();
}
+ }
- /**
- * Checks if the OpenTelemetry collector is reachable.
- */
- private boolean checkCollectorConnectivity() {
- try {
- String endpoint = properties.getTraces().getEndpoint();
- URI uri = URI.create(endpoint);
+ /** Checks if the OpenTelemetry collector is reachable. */
+ private boolean checkCollectorConnectivity() {
+ try {
+ String endpoint = properties.traces().endpoint();
+ URI uri = URI.create(endpoint);
- String host = uri.getHost();
- int port = uri.getPort();
+ String host = uri.getHost();
+ int port = uri.getPort();
- // Default OTLP gRPC port
- if (port == -1) {
- port = 4317;
- }
+ // Default OTLP gRPC port
+ if (port == -1) {
+ port = 4317;
+ }
- // Try to establish a socket connection
- try (Socket socket = new Socket(host, port)) {
- return socket.isConnected();
- }
- } catch (IOException e) {
- logger.debug("OpenTelemetry collector not reachable: {}", e.getMessage());
- return false;
- } catch (Exception e) {
- logger.warn("Error checking OpenTelemetry collector connectivity: {}", e.getMessage());
- return false;
- }
+ // Probe TCP reachability of the collector without opening a data channel.
+ try (SocketChannel channel = SocketChannel.open()) {
+ return channel.connect(new InetSocketAddress(host, port));
+ }
+ } catch (IOException e) {
+ logger.debug(
+ "OpenTelemetry collector not reachable: {}", LogSanitizer.sanitize(e.getMessage()));
+ return false;
+ } catch (Exception e) {
+ logger.warn(
+ "Error checking OpenTelemetry collector connectivity: {}",
+ LogSanitizer.sanitize(e.getMessage()));
+ return false;
}
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/QueryConfig.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/QueryConfig.java
index b264d63..e0a9c9c 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/QueryConfig.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/QueryConfig.java
@@ -8,68 +8,23 @@
import org.springframework.validation.annotation.Validated;
/**
- * Configuration properties for query processing settings.
+ * Immutable configuration properties for query processing settings.
+ *
+ * Populated through Spring Boot constructor binding; construct directly with the canonical
+ * constructor in tests.
*/
@ConfigurationProperties(prefix = "query")
@Validated
-public class QueryConfig {
-
+public record QueryConfig(
@NotNull(message = "Max retrieved chunks must be specified")
- @Positive(message = "Max retrieved chunks must be positive")
- private Integer maxRetrievedChunks;
-
+ @Positive(message = "Max retrieved chunks must be positive")
+ Integer maxRetrievedChunks,
@NotNull(message = "Similarity threshold must be specified")
- @DecimalMin(value = "0.0", message = "Similarity threshold must be at least 0.0")
- @DecimalMax(value = "1.0", message = "Similarity threshold must be at most 1.0")
- private Double similarityThreshold;
-
+ @DecimalMin(value = "0.0", message = "Similarity threshold must be at least 0.0")
+ @DecimalMax(value = "1.0", message = "Similarity threshold must be at most 1.0")
+ Double similarityThreshold,
@NotNull(message = "Timeout seconds must be specified")
- @Positive(message = "Timeout seconds must be positive")
- private Integer timeoutSeconds;
-
- private Boolean enableCache;
-
- private Integer cacheTtlMinutes;
-
- // Getters and Setters
-
- public Integer getMaxRetrievedChunks() {
- return maxRetrievedChunks;
- }
-
- public void setMaxRetrievedChunks(Integer maxRetrievedChunks) {
- this.maxRetrievedChunks = maxRetrievedChunks;
- }
-
- public Double getSimilarityThreshold() {
- return similarityThreshold;
- }
-
- public void setSimilarityThreshold(Double similarityThreshold) {
- this.similarityThreshold = similarityThreshold;
- }
-
- public Integer getTimeoutSeconds() {
- return timeoutSeconds;
- }
-
- public void setTimeoutSeconds(Integer timeoutSeconds) {
- this.timeoutSeconds = timeoutSeconds;
- }
-
- public Boolean getEnableCache() {
- return enableCache;
- }
-
- public void setEnableCache(Boolean enableCache) {
- this.enableCache = enableCache;
- }
-
- public Integer getCacheTtlMinutes() {
- return cacheTtlMinutes;
- }
-
- public void setCacheTtlMinutes(Integer cacheTtlMinutes) {
- this.cacheTtlMinutes = cacheTtlMinutes;
- }
-}
+ @Positive(message = "Timeout seconds must be positive")
+ Integer timeoutSeconds,
+ Boolean enableCache,
+ Integer cacheTtlMinutes) {}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/RagSystemConfiguration.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/RagSystemConfiguration.java
index a1d0efd..5894067 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/RagSystemConfiguration.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/RagSystemConfiguration.java
@@ -3,199 +3,206 @@
import br.com.arquivolivre.myjavagenie.repository.VectorRepository;
import br.com.arquivolivre.myjavagenie.repository.VectorRepositoryFactory;
import br.com.arquivolivre.myjavagenie.service.*;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
import io.opentelemetry.api.trace.Tracer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.lang.Nullable;
/**
- * Main configuration class for the RAG System.
- * Defines all major component beans with proper dependency injection.
- * Uses @ConditionalOnProperty for optional features.
+ * Main configuration class for the RAG System. Defines all major component beans with proper
+ * dependency injection. Uses @ConditionalOnProperty for optional features.
*/
@Configuration
@EnableConfigurationProperties({
- ModelConfig.class,
- VectorDbConfig.class,
- IngestionConfig.class,
- QueryConfig.class
+ ModelConfig.class,
+ VectorDbConfig.class,
+ IngestionConfig.class,
+ QueryConfig.class
})
public class RagSystemConfiguration {
- private static final Logger logger = LoggerFactory.getLogger(RagSystemConfiguration.class);
-
- /**
- * Creates the LanguageModelFactory bean.
- * This factory is responsible for creating language model providers based on configuration.
- */
- @Bean
- public LanguageModelFactory languageModelFactory() {
- logger.info("Initializing LanguageModelFactory bean");
- return new DefaultLanguageModelFactory();
- }
-
- /**
- * Creates the LanguageModelProvider bean.
- * This is the actual language model provider instance used throughout the application.
- *
- * @param languageModelFactory the factory to create the provider
- * @param modelConfig the model configuration
- * @return configured LanguageModelProvider
- */
- @Bean
- public LanguageModelProvider languageModelProvider(
- LanguageModelFactory languageModelFactory,
- ModelConfig modelConfig) {
- logger.info("Initializing LanguageModelProvider bean for provider: {}", modelConfig.getProvider());
- return languageModelFactory.createProvider(modelConfig);
- }
-
- /**
- * Creates the EmbeddingModelProvider bean.
- * This provider generates embeddings for text chunks and queries.
- */
- @Bean
- public EmbeddingModelProvider embeddingModelProvider() {
- logger.info("Initializing EmbeddingModelProvider bean");
- return new DefaultEmbeddingModelProvider();
- }
-
- /**
- * Creates the VectorRepositoryFactory bean.
- * This factory creates vector repository instances based on configuration.
- */
- @Bean
- public VectorRepositoryFactory vectorRepositoryFactory() {
- logger.info("Initializing VectorRepositoryFactory bean");
- return new VectorRepositoryFactory();
- }
-
- /**
- * Creates the VectorRepository bean.
- * This is the actual vector database repository instance used throughout the application.
- *
- * @param vectorRepositoryFactory the factory to create the repository
- * @param vectorDbConfig the vector database configuration
- * @return configured VectorRepository
- */
- @Bean
- public VectorRepository vectorRepository(
- VectorRepositoryFactory vectorRepositoryFactory,
- VectorDbConfig vectorDbConfig) {
- logger.info("Initializing VectorRepository bean for type: {}", vectorDbConfig.getType());
- return vectorRepositoryFactory.createRepository(vectorDbConfig);
- }
-
- /**
- * Creates the DocumentProcessor bean.
- * This processor chunks documents into smaller pieces for embedding and retrieval.
- *
- * @param ingestionConfig the ingestion configuration
- * @return configured DocumentProcessor
- */
- @Bean
- public DocumentProcessor documentProcessor(IngestionConfig ingestionConfig) {
- logger.info("Initializing DocumentProcessor bean with chunk size: {}, overlap: {}",
- ingestionConfig.getChunkSize(), ingestionConfig.getChunkOverlap());
- return new RecursiveCharacterSplitter(ingestionConfig);
- }
-
- /**
- * Creates the DocumentLoader bean.
- * This loader reads documents from the filesystem.
- */
- @Bean
- public DocumentLoader documentLoader() {
- logger.info("Initializing DocumentLoader bean");
- return new DocumentLoader();
- }
-
- /**
- * Creates the RetrievalEngine bean.
- * This engine retrieves relevant document chunks based on query similarity.
- *
- * @param vectorRepository the vector repository for similarity search
- * @param embeddingModelProvider the embedding model for query embedding
- * @param queryConfig the query configuration
- * @return configured RetrievalEngine
- */
- @Bean
- public RetrievalEngine retrievalEngine(
- VectorRepository vectorRepository,
- EmbeddingModelProvider embeddingModelProvider,
- QueryConfig queryConfig,
- @Autowired(required = false) Tracer tracer) {
- logger.info("Initializing RetrievalEngine bean with max chunks: {}, threshold: {}",
- queryConfig.getMaxRetrievedChunks(), queryConfig.getSimilarityThreshold());
- return new RetrievalEngine(vectorRepository, embeddingModelProvider, queryConfig, tracer);
- }
-
- /**
- * Creates the PromptBuilder bean.
- * This builder constructs prompts for the language model with retrieved context.
- */
- @Bean
- public PromptBuilder promptBuilder() {
- logger.info("Initializing PromptBuilder bean");
- return new PromptBuilder();
- }
-
- /**
- * Creates the TokenUsageTracker bean.
- * This tracker monitors and logs token consumption for cost analysis.
- */
- @Bean
- public TokenUsageTracker tokenUsageTracker() {
- logger.info("Initializing TokenUsageTracker bean");
- return new TokenUsageTracker();
- }
-
- /**
- * Creates the ConfigurationProvider bean.
- * This provider wraps all configuration properties and provides validation.
- *
- * @param modelConfig the model configuration
- * @param vectorDbConfig the vector database configuration
- * @param ingestionConfig the ingestion configuration
- * @param queryConfig the query configuration
- * @return configured ConfigurationProvider
- */
- @Bean
- public ConfigurationProvider configurationProvider(
- ModelConfig modelConfig,
- VectorDbConfig vectorDbConfig,
- IngestionConfig ingestionConfig,
- QueryConfig queryConfig) {
- logger.info("Initializing ConfigurationProvider bean");
- return new SpringConfigurationProvider(modelConfig, vectorDbConfig, ingestionConfig, queryConfig);
- }
-
- /**
- * Optional bean for development mode features.
- * Only created when 'rag.dev-mode.enabled' property is set to true.
- */
- @Bean
- @ConditionalOnProperty(name = "rag.dev-mode.enabled", havingValue = "true", matchIfMissing = false)
- public DevModeConfiguration devModeConfiguration() {
- logger.info("Development mode enabled - initializing DevModeConfiguration bean");
- return new DevModeConfiguration();
- }
-
- /**
- * Inner class for development mode specific configuration.
- * This can include features like verbose logging, test data generation, etc.
- */
- public static class DevModeConfiguration {
- private static final Logger devLogger = LoggerFactory.getLogger(DevModeConfiguration.class);
-
- public DevModeConfiguration() {
- devLogger.warn("=== DEVELOPMENT MODE ACTIVE ===");
- devLogger.warn("This mode should NOT be used in production");
- devLogger.warn("Additional logging and debugging features are enabled");
- }
+ private static final Logger logger = LoggerFactory.getLogger(RagSystemConfiguration.class);
+
+ /**
+ * Creates the LanguageModelFactory bean. This factory is responsible for creating language model
+ * providers based on configuration.
+ */
+ @Bean
+ public LanguageModelFactory languageModelFactory() {
+ logger.info("Initializing LanguageModelFactory bean");
+ return new DefaultLanguageModelFactory();
+ }
+
+ /**
+ * Creates the LanguageModelProvider bean. This is the actual language model provider instance
+ * used throughout the application.
+ *
+ * @param languageModelFactory the factory to create the provider
+ * @param modelConfig the model configuration
+ * @return configured LanguageModelProvider
+ */
+ @Bean
+ public LanguageModelProvider languageModelProvider(
+ LanguageModelFactory languageModelFactory, ModelConfig modelConfig) {
+ logger.info(
+ "Initializing LanguageModelProvider bean for provider: {}",
+ LogSanitizer.sanitize(modelConfig.provider()));
+ return languageModelFactory.createProvider(modelConfig);
+ }
+
+ /**
+ * Creates the EmbeddingModelProvider bean. This provider generates embeddings for text chunks and
+ * queries.
+ */
+ @Bean
+ public EmbeddingModelProvider embeddingModelProvider() {
+ logger.info("Initializing EmbeddingModelProvider bean");
+ return new DefaultEmbeddingModelProvider();
+ }
+
+ /**
+ * Creates the VectorRepositoryFactory bean. This factory creates vector repository instances
+ * based on configuration.
+ */
+ @Bean
+ public VectorRepositoryFactory vectorRepositoryFactory() {
+ logger.info("Initializing VectorRepositoryFactory bean");
+ return new VectorRepositoryFactory();
+ }
+
+ /**
+ * Creates the VectorRepository bean. This is the actual vector database repository instance used
+ * throughout the application.
+ *
+ * @param vectorRepositoryFactory the factory to create the repository
+ * @param vectorDbConfig the vector database configuration
+ * @return configured VectorRepository
+ */
+ @Bean
+ public VectorRepository vectorRepository(
+ VectorRepositoryFactory vectorRepositoryFactory, VectorDbConfig vectorDbConfig) {
+ logger.info(
+ "Initializing VectorRepository bean for type: {}",
+ LogSanitizer.sanitize(vectorDbConfig.type()));
+ return vectorRepositoryFactory.createRepository(vectorDbConfig);
+ }
+
+ /**
+ * Creates the DocumentProcessor bean. This processor chunks documents into smaller pieces for
+ * embedding and retrieval.
+ *
+ * @param ingestionConfig the ingestion configuration
+ * @return configured DocumentProcessor
+ */
+ @Bean
+ public DocumentProcessor documentProcessor(IngestionConfig ingestionConfig) {
+ logger.info(
+ "Initializing DocumentProcessor bean with chunk size: {}, overlap: {}",
+ LogSanitizer.sanitize(ingestionConfig.chunkSize()),
+ LogSanitizer.sanitize(ingestionConfig.chunkOverlap()));
+ return new RecursiveCharacterSplitter(ingestionConfig);
+ }
+
+ /** Creates the DocumentLoader bean. This loader reads documents from the filesystem. */
+ @Bean
+ public DocumentLoader documentLoader() {
+ logger.info("Initializing DocumentLoader bean");
+ return new DocumentLoader();
+ }
+
+ /**
+ * Creates the RetrievalEngine bean. This engine retrieves relevant document chunks based on query
+ * similarity.
+ *
+ * @param vectorRepository the vector repository for similarity search
+ * @param embeddingModelProvider the embedding model for query embedding
+ * @param queryConfig the query configuration
+ * @return configured RetrievalEngine
+ */
+ @Bean
+ public RetrievalEngine retrievalEngine(
+ VectorRepository vectorRepository,
+ EmbeddingModelProvider embeddingModelProvider,
+ QueryConfig queryConfig,
+ @Nullable Tracer tracer) {
+ logger.info(
+ "Initializing RetrievalEngine bean with max chunks: {}, threshold: {}",
+ LogSanitizer.sanitize(queryConfig.maxRetrievedChunks()),
+ LogSanitizer.sanitize(queryConfig.similarityThreshold()));
+ return new RetrievalEngine(vectorRepository, embeddingModelProvider, queryConfig, tracer);
+ }
+
+ /**
+ * Creates the PromptBuilder bean. This builder constructs prompts for the language model with
+ * retrieved context.
+ */
+ @Bean
+ public PromptBuilder promptBuilder() {
+ logger.info("Initializing PromptBuilder bean");
+ return new PromptBuilder();
+ }
+
+ /**
+ * Creates the TokenUsageTracker bean. This tracker monitors and logs token consumption for cost
+ * analysis.
+ */
+ @Bean
+ public TokenUsageTracker tokenUsageTracker() {
+ logger.info("Initializing TokenUsageTracker bean");
+ return new TokenUsageTracker();
+ }
+
+ /**
+ * Creates the ConfigurationProvider bean. This provider wraps all configuration properties and
+ * provides validation.
+ *
+ * @param modelConfig the model configuration
+ * @param vectorDbConfig the vector database configuration
+ * @param ingestionConfig the ingestion configuration
+ * @param queryConfig the query configuration
+ * @return configured ConfigurationProvider
+ */
+ @Bean
+ public ConfigurationProvider configurationProvider(
+ ModelConfig modelConfig,
+ VectorDbConfig vectorDbConfig,
+ IngestionConfig ingestionConfig,
+ QueryConfig queryConfig) {
+ logger.info("Initializing ConfigurationProvider bean");
+ return new SpringConfigurationProvider(
+ modelConfig, vectorDbConfig, ingestionConfig, queryConfig);
+ }
+
+ /**
+ * Optional bean for development mode features. Only created when 'rag.dev-mode.enabled' property
+ * is set to true.
+ */
+ @Bean
+ @ConditionalOnProperty(
+ name = "rag.dev-mode.enabled",
+ havingValue = "true",
+ matchIfMissing = false)
+ public DevModeConfiguration devModeConfiguration() {
+ logger.info("Development mode enabled - initializing DevModeConfiguration bean");
+ return new DevModeConfiguration();
+ }
+
+ /**
+ * Inner class for development mode specific configuration. This can include features like verbose
+ * logging, test data generation, etc.
+ */
+ public static class DevModeConfiguration {
+ private static final Logger devLogger = LoggerFactory.getLogger(DevModeConfiguration.class);
+
+ public DevModeConfiguration() {
+ devLogger.warn("=== DEVELOPMENT MODE ACTIVE ===");
+ devLogger.warn("This mode should NOT be used in production");
+ devLogger.warn("Additional logging and debugging features are enabled");
}
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/SpringConfigurationProvider.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/SpringConfigurationProvider.java
index 3a55c00..43d0e81 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/SpringConfigurationProvider.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/SpringConfigurationProvider.java
@@ -1,232 +1,237 @@
package br.com.arquivolivre.myjavagenie.config;
import br.com.arquivolivre.myjavagenie.exception.ConfigurationException;
+import java.util.Locale;
import org.springframework.stereotype.Component;
/**
- * Spring-based implementation of ConfigurationProvider.
- * Wraps Spring's @ConfigurationProperties beans and provides validation.
+ * Spring-based implementation of ConfigurationProvider. Wraps Spring's @ConfigurationProperties
+ * beans and provides validation.
*/
@Component
public class SpringConfigurationProvider implements ConfigurationProvider {
- private final ModelConfig modelConfig;
- private final VectorDbConfig vectorDbConfig;
- private final IngestionConfig ingestionConfig;
- private final QueryConfig queryConfig;
-
- public SpringConfigurationProvider(
- ModelConfig modelConfig,
- VectorDbConfig vectorDbConfig,
- IngestionConfig ingestionConfig,
- QueryConfig queryConfig) {
- this.modelConfig = modelConfig;
- this.vectorDbConfig = vectorDbConfig;
- this.ingestionConfig = ingestionConfig;
- this.queryConfig = queryConfig;
- }
-
- @Override
- public ModelConfig getModelConfig() {
- return modelConfig;
- }
-
- @Override
- public VectorDbConfig getVectorDbConfig() {
- return vectorDbConfig;
- }
-
- @Override
- public IngestionConfig getIngestionConfig() {
- return ingestionConfig;
- }
-
- @Override
- public QueryConfig getQueryConfig() {
- return queryConfig;
- }
-
- @Override
- public void validateConfiguration() {
- validateModelConfig();
- validateVectorDbConfig();
- validateIngestionConfig();
- validateQueryConfig();
- }
-
- private void validateModelConfig() {
- if (modelConfig == null) {
- throw new ConfigurationException("Model configuration is missing");
- }
-
- String provider = modelConfig.getProvider();
- if (provider == null || provider.isBlank()) {
- throw new ConfigurationException("Model provider must be specified");
- }
-
- // Validate provider-specific settings
- switch (provider.toLowerCase()) {
- case "self-hosted":
- validateSelfHostedConfig();
- break;
- case "openai":
- validateOpenAIConfig();
- break;
- case "anthropic":
- validateAnthropicConfig();
- break;
- default:
- throw new ConfigurationException(
- "Unsupported model provider: " + provider + ". Supported providers: self-hosted, openai, anthropic");
- }
-
- // Validate common settings
- if (modelConfig.getTemperature() == null) {
- throw new ConfigurationException("Model temperature must be specified");
- }
- if (modelConfig.getTemperature() < 0.0 || modelConfig.getTemperature() > 2.0) {
- throw new ConfigurationException("Model temperature must be between 0.0 and 2.0");
- }
-
- if (modelConfig.getMaxTokens() == null || modelConfig.getMaxTokens() <= 0) {
- throw new ConfigurationException("Model max tokens must be a positive number");
- }
- }
-
- private void validateSelfHostedConfig() {
- ModelConfig.SelfHostedSettings settings = modelConfig.getSelfHosted();
- if (settings == null) {
- throw new ConfigurationException("Self-hosted model settings are missing");
- }
- if (settings.getBaseUrl() == null || settings.getBaseUrl().isBlank()) {
- throw new ConfigurationException("Self-hosted model base URL must be specified");
- }
- if (settings.getModelName() == null || settings.getModelName().isBlank()) {
- throw new ConfigurationException("Self-hosted model name must be specified");
- }
- }
-
- private void validateOpenAIConfig() {
- ModelConfig.OpenAISettings settings = modelConfig.getOpenai();
- if (settings == null) {
- throw new ConfigurationException("OpenAI settings are missing");
- }
- if (settings.getApiKey() == null || settings.getApiKey().isBlank()) {
- throw new ConfigurationException("OpenAI API key must be specified");
- }
- if (settings.getModelName() == null || settings.getModelName().isBlank()) {
- throw new ConfigurationException("OpenAI model name must be specified");
- }
- }
-
- private void validateAnthropicConfig() {
- ModelConfig.AnthropicSettings settings = modelConfig.getAnthropic();
- if (settings == null) {
- throw new ConfigurationException("Anthropic settings are missing");
- }
- if (settings.getApiKey() == null || settings.getApiKey().isBlank()) {
- throw new ConfigurationException("Anthropic API key must be specified");
- }
- if (settings.getModelName() == null || settings.getModelName().isBlank()) {
- throw new ConfigurationException("Anthropic model name must be specified");
- }
- }
-
- private void validateVectorDbConfig() {
- if (vectorDbConfig == null) {
- throw new ConfigurationException("Vector database configuration is missing");
- }
-
- if (vectorDbConfig.getType() == null || vectorDbConfig.getType().isBlank()) {
- throw new ConfigurationException("Vector database type must be specified");
- }
-
- if (vectorDbConfig.getConnectionUrl() == null || vectorDbConfig.getConnectionUrl().isBlank()) {
- throw new ConfigurationException("Vector database connection URL must be specified");
- }
-
- if (vectorDbConfig.getCollectionName() == null || vectorDbConfig.getCollectionName().isBlank()) {
- throw new ConfigurationException("Vector database collection name must be specified");
- }
-
- // Validate type-specific settings
- String type = vectorDbConfig.getType().toLowerCase();
- switch (type) {
- case "chroma":
- // ChromaDB settings are optional
- break;
- case "pgvector":
- validatePgVectorConfig();
- break;
- case "qdrant":
- // Qdrant settings are optional (API key and TLS)
- break;
- default:
- throw new ConfigurationException(
- "Unsupported vector database type: " + type + ". Supported types: chroma, pgvector, qdrant");
- }
- }
-
- private void validatePgVectorConfig() {
- VectorDbConfig.PgVectorSettings settings = vectorDbConfig.getPgvector();
- if (settings == null) {
- throw new ConfigurationException("pgvector settings are missing");
- }
- if (settings.getHost() == null || settings.getHost().isBlank()) {
- throw new ConfigurationException("pgvector host must be specified");
- }
- if (settings.getPort() == null || settings.getPort() <= 0) {
- throw new ConfigurationException("pgvector port must be a positive number");
- }
- if (settings.getDatabase() == null || settings.getDatabase().isBlank()) {
- throw new ConfigurationException("pgvector database must be specified");
- }
- if (settings.getUsername() == null || settings.getUsername().isBlank()) {
- throw new ConfigurationException("pgvector username must be specified");
- }
- }
-
- private void validateIngestionConfig() {
- if (ingestionConfig == null) {
- throw new ConfigurationException("Ingestion configuration is missing");
- }
-
- if (ingestionConfig.getChunkSize() == null || ingestionConfig.getChunkSize() <= 0) {
- throw new ConfigurationException("Ingestion chunk size must be a positive number");
- }
-
- if (ingestionConfig.getChunkOverlap() == null || ingestionConfig.getChunkOverlap() < 0) {
- throw new ConfigurationException("Ingestion chunk overlap must be zero or positive");
- }
-
- if (ingestionConfig.getChunkOverlap() >= ingestionConfig.getChunkSize()) {
- throw new ConfigurationException("Ingestion chunk overlap must be less than chunk size");
- }
-
- if (ingestionConfig.getBatchSize() == null || ingestionConfig.getBatchSize() <= 0) {
- throw new ConfigurationException("Ingestion batch size must be a positive number");
- }
- }
-
- private void validateQueryConfig() {
- if (queryConfig == null) {
- throw new ConfigurationException("Query configuration is missing");
- }
-
- if (queryConfig.getMaxRetrievedChunks() == null || queryConfig.getMaxRetrievedChunks() <= 0) {
- throw new ConfigurationException("Query max retrieved chunks must be a positive number");
- }
-
- if (queryConfig.getSimilarityThreshold() == null) {
- throw new ConfigurationException("Query similarity threshold must be specified");
- }
-
- if (queryConfig.getSimilarityThreshold() < 0.0 || queryConfig.getSimilarityThreshold() > 1.0) {
- throw new ConfigurationException("Query similarity threshold must be between 0.0 and 1.0");
- }
-
- if (queryConfig.getTimeoutSeconds() == null || queryConfig.getTimeoutSeconds() <= 0) {
- throw new ConfigurationException("Query timeout seconds must be a positive number");
- }
+ private final ModelConfig modelConfig;
+ private final VectorDbConfig vectorDbConfig;
+ private final IngestionConfig ingestionConfig;
+ private final QueryConfig queryConfig;
+
+ public SpringConfigurationProvider(
+ ModelConfig modelConfig,
+ VectorDbConfig vectorDbConfig,
+ IngestionConfig ingestionConfig,
+ QueryConfig queryConfig) {
+ this.modelConfig = modelConfig;
+ this.vectorDbConfig = vectorDbConfig;
+ this.ingestionConfig = ingestionConfig;
+ this.queryConfig = queryConfig;
+ }
+
+ @Override
+ public ModelConfig getModelConfig() {
+ return modelConfig;
+ }
+
+ @Override
+ public VectorDbConfig getVectorDbConfig() {
+ return vectorDbConfig;
+ }
+
+ @Override
+ public IngestionConfig getIngestionConfig() {
+ return ingestionConfig;
+ }
+
+ @Override
+ public QueryConfig getQueryConfig() {
+ return queryConfig;
+ }
+
+ @Override
+ public void validateConfiguration() {
+ validateModelConfig();
+ validateVectorDbConfig();
+ validateIngestionConfig();
+ validateQueryConfig();
+ }
+
+ private void validateModelConfig() {
+ if (modelConfig == null) {
+ throw new ConfigurationException("Model configuration is missing");
}
+
+ String provider = modelConfig.provider();
+ if (provider == null || provider.isBlank()) {
+ throw new ConfigurationException("Model provider must be specified");
+ }
+
+ // Validate provider-specific settings
+ switch (provider.toLowerCase(Locale.ROOT)) {
+ case "self-hosted":
+ validateSelfHostedConfig();
+ break;
+ case "openai":
+ validateOpenAIConfig();
+ break;
+ case "anthropic":
+ validateAnthropicConfig();
+ break;
+ default:
+ throw new ConfigurationException(
+ "Unsupported model provider: "
+ + provider
+ + ". Supported providers: self-hosted, openai, anthropic");
+ }
+
+ // Validate common settings
+ if (modelConfig.temperature() == null) {
+ throw new ConfigurationException("Model temperature must be specified");
+ }
+ if (modelConfig.temperature() < 0.0 || modelConfig.temperature() > 2.0) {
+ throw new ConfigurationException("Model temperature must be between 0.0 and 2.0");
+ }
+
+ if (modelConfig.maxTokens() == null || modelConfig.maxTokens() <= 0) {
+ throw new ConfigurationException("Model max tokens must be a positive number");
+ }
+ }
+
+ private void validateSelfHostedConfig() {
+ ModelConfig.SelfHostedSettings settings = modelConfig.selfHosted();
+ if (settings == null) {
+ throw new ConfigurationException("Self-hosted model settings are missing");
+ }
+ if (settings.baseUrl() == null || settings.baseUrl().isBlank()) {
+ throw new ConfigurationException("Self-hosted model base URL must be specified");
+ }
+ if (settings.modelName() == null || settings.modelName().isBlank()) {
+ throw new ConfigurationException("Self-hosted model name must be specified");
+ }
+ }
+
+ private void validateOpenAIConfig() {
+ ModelConfig.OpenAISettings settings = modelConfig.openai();
+ if (settings == null) {
+ throw new ConfigurationException("OpenAI settings are missing");
+ }
+ if (settings.apiKey() == null || settings.apiKey().isBlank()) {
+ throw new ConfigurationException("OpenAI API key must be specified");
+ }
+ if (settings.modelName() == null || settings.modelName().isBlank()) {
+ throw new ConfigurationException("OpenAI model name must be specified");
+ }
+ }
+
+ private void validateAnthropicConfig() {
+ ModelConfig.AnthropicSettings settings = modelConfig.anthropic();
+ if (settings == null) {
+ throw new ConfigurationException("Anthropic settings are missing");
+ }
+ if (settings.apiKey() == null || settings.apiKey().isBlank()) {
+ throw new ConfigurationException("Anthropic API key must be specified");
+ }
+ if (settings.modelName() == null || settings.modelName().isBlank()) {
+ throw new ConfigurationException("Anthropic model name must be specified");
+ }
+ }
+
+ private void validateVectorDbConfig() {
+ if (vectorDbConfig == null) {
+ throw new ConfigurationException("Vector database configuration is missing");
+ }
+
+ if (vectorDbConfig.type() == null || vectorDbConfig.type().isBlank()) {
+ throw new ConfigurationException("Vector database type must be specified");
+ }
+
+ if (vectorDbConfig.connectionUrl() == null || vectorDbConfig.connectionUrl().isBlank()) {
+ throw new ConfigurationException("Vector database connection URL must be specified");
+ }
+
+ if (vectorDbConfig.collectionName() == null || vectorDbConfig.collectionName().isBlank()) {
+ throw new ConfigurationException("Vector database collection name must be specified");
+ }
+
+ // Validate type-specific settings
+ String type = vectorDbConfig.type().toLowerCase(Locale.ROOT);
+ switch (type) {
+ case "chroma":
+ // ChromaDB settings are optional
+ break;
+ case "pgvector":
+ validatePgVectorConfig();
+ break;
+ case "qdrant":
+ // Qdrant settings are optional (API key and TLS)
+ break;
+ default:
+ throw new ConfigurationException(
+ "Unsupported vector database type: "
+ + type
+ + ". Supported types: chroma, pgvector, qdrant");
+ }
+ }
+
+ private void validatePgVectorConfig() {
+ VectorDbConfig.PgVectorSettings settings = vectorDbConfig.pgvector();
+ if (settings == null) {
+ throw new ConfigurationException("pgvector settings are missing");
+ }
+ if (settings.host() == null || settings.host().isBlank()) {
+ throw new ConfigurationException("pgvector host must be specified");
+ }
+ if (settings.port() == null || settings.port() <= 0) {
+ throw new ConfigurationException("pgvector port must be a positive number");
+ }
+ if (settings.database() == null || settings.database().isBlank()) {
+ throw new ConfigurationException("pgvector database must be specified");
+ }
+ if (settings.username() == null || settings.username().isBlank()) {
+ throw new ConfigurationException("pgvector username must be specified");
+ }
+ }
+
+ private void validateIngestionConfig() {
+ if (ingestionConfig == null) {
+ throw new ConfigurationException("Ingestion configuration is missing");
+ }
+
+ if (ingestionConfig.chunkSize() == null || ingestionConfig.chunkSize() <= 0) {
+ throw new ConfigurationException("Ingestion chunk size must be a positive number");
+ }
+
+ if (ingestionConfig.chunkOverlap() == null || ingestionConfig.chunkOverlap() < 0) {
+ throw new ConfigurationException("Ingestion chunk overlap must be zero or positive");
+ }
+
+ if (ingestionConfig.chunkOverlap() >= ingestionConfig.chunkSize()) {
+ throw new ConfigurationException("Ingestion chunk overlap must be less than chunk size");
+ }
+
+ if (ingestionConfig.batchSize() == null || ingestionConfig.batchSize() <= 0) {
+ throw new ConfigurationException("Ingestion batch size must be a positive number");
+ }
+ }
+
+ private void validateQueryConfig() {
+ if (queryConfig == null) {
+ throw new ConfigurationException("Query configuration is missing");
+ }
+
+ if (queryConfig.maxRetrievedChunks() == null || queryConfig.maxRetrievedChunks() <= 0) {
+ throw new ConfigurationException("Query max retrieved chunks must be a positive number");
+ }
+
+ if (queryConfig.similarityThreshold() == null) {
+ throw new ConfigurationException("Query similarity threshold must be specified");
+ }
+
+ if (queryConfig.similarityThreshold() < 0.0 || queryConfig.similarityThreshold() > 1.0) {
+ throw new ConfigurationException("Query similarity threshold must be between 0.0 and 1.0");
+ }
+
+ if (queryConfig.timeoutSeconds() == null || queryConfig.timeoutSeconds() <= 0) {
+ throw new ConfigurationException("Query timeout seconds must be a positive number");
+ }
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/TraceContextMdcFilter.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/TraceContextMdcFilter.java
index bad3204..0d7f904 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/TraceContextMdcFilter.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/TraceContextMdcFilter.java
@@ -6,47 +6,46 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
-import java.io.IOException;
-
/**
- * Filter that populates MDC (Mapped Diagnostic Context) with OpenTelemetry trace information.
- * This enables log correlation by including trace_id and span_id in all log statements.
+ * Filter that populates MDC (Mapped Diagnostic Context) with OpenTelemetry trace information. This
+ * enables log correlation by including trace_id and span_id in all log statements.
*/
@Component
@Order(1)
@ConditionalOnProperty(name = "opentelemetry.enabled", havingValue = "true", matchIfMissing = false)
public class TraceContextMdcFilter extends OncePerRequestFilter {
- private static final String TRACE_ID_KEY = "trace_id";
- private static final String SPAN_ID_KEY = "span_id";
+ private static final String TRACE_ID_KEY = "trace_id";
+ private static final String SPAN_ID_KEY = "span_id";
- @Override
- protected void doFilterInternal(HttpServletRequest request,
- HttpServletResponse response,
- FilterChain filterChain) throws ServletException, IOException {
- try {
- // Get current span context
- Span currentSpan = Span.current();
- SpanContext spanContext = currentSpan.getSpanContext();
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ try {
+ // Get current span context
+ Span currentSpan = Span.current();
+ SpanContext spanContext = currentSpan.getSpanContext();
- // Add trace and span IDs to MDC if available
- if (spanContext.isValid()) {
- MDC.put(TRACE_ID_KEY, spanContext.getTraceId());
- MDC.put(SPAN_ID_KEY, spanContext.getSpanId());
- }
+ // Add trace and span IDs to MDC if available
+ if (spanContext.isValid()) {
+ MDC.put(TRACE_ID_KEY, spanContext.getTraceId());
+ MDC.put(SPAN_ID_KEY, spanContext.getSpanId());
+ }
- // Continue with the filter chain
- filterChain.doFilter(request, response);
- } finally {
- // Clean up MDC after request processing
- MDC.remove(TRACE_ID_KEY);
- MDC.remove(SPAN_ID_KEY);
- }
+ // Continue with the filter chain
+ filterChain.doFilter(request, response);
+ } finally {
+ // Clean up MDC after request processing
+ MDC.remove(TRACE_ID_KEY);
+ MDC.remove(SPAN_ID_KEY);
}
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/VectorDbConfig.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/VectorDbConfig.java
index adb468d..1c7b1bd 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/VectorDbConfig.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/VectorDbConfig.java
@@ -7,206 +7,35 @@
import org.springframework.validation.annotation.Validated;
/**
- * Configuration properties for vector database settings.
- * Supports ChromaDB, pgvector, and Qdrant.
+ * Immutable configuration properties for vector database settings. Supports ChromaDB, pgvector, and
+ * Qdrant.
+ *
+ * Populated through Spring Boot constructor binding; construct directly with the canonical
+ * constructor in tests.
*/
@ConfigurationProperties(prefix = "vector-db")
@Validated
-public class VectorDbConfig {
-
- @NotBlank(message = "Vector database type must be specified")
- private String type;
-
- @NotBlank(message = "Connection URL must be specified")
- private String connectionUrl;
-
- @NotBlank(message = "Collection name must be specified")
- private String collectionName;
-
- @Valid
- private ChromaSettings chroma;
-
- @Valid
- private PgVectorSettings pgvector;
-
- @Valid
- private QdrantSettings qdrant;
-
- // Getters and Setters
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
- public String getConnectionUrl() {
- return connectionUrl;
- }
-
- public void setConnectionUrl(String connectionUrl) {
- this.connectionUrl = connectionUrl;
- }
-
- public String getCollectionName() {
- return collectionName;
- }
-
- public void setCollectionName(String collectionName) {
- this.collectionName = collectionName;
- }
-
- public ChromaSettings getChroma() {
- return chroma;
- }
-
- public void setChroma(ChromaSettings chroma) {
- this.chroma = chroma;
- }
-
- public PgVectorSettings getPgvector() {
- return pgvector;
- }
-
- public void setPgvector(PgVectorSettings pgvector) {
- this.pgvector = pgvector;
- }
-
- public QdrantSettings getQdrant() {
- return qdrant;
- }
-
- public void setQdrant(QdrantSettings qdrant) {
- this.qdrant = qdrant;
- }
-
- /**
- * Configuration for ChromaDB.
- */
- public static class ChromaSettings {
- private String tenant;
- private String database;
-
- public String getTenant() {
- return tenant;
- }
-
- public void setTenant(String tenant) {
- this.tenant = tenant;
- }
-
- public String getDatabase() {
- return database;
- }
-
- public void setDatabase(String database) {
- this.database = database;
- }
- }
-
- /**
- * Configuration for PostgreSQL with pgvector extension.
- */
- public static class PgVectorSettings {
- @NotBlank(message = "PostgreSQL host must be specified")
- private String host;
-
- @Positive(message = "PostgreSQL port must be positive")
- private Integer port;
-
- @NotBlank(message = "PostgreSQL database must be specified")
- private String database;
-
- @NotBlank(message = "PostgreSQL username must be specified")
- private String username;
-
- private String password;
-
- private String schema;
-
- @NotBlank(message = "PostgreSQL table name must be specified")
- private String tableName;
-
- public String getHost() {
- return host;
- }
-
- public void setHost(String host) {
- this.host = host;
- }
-
- public Integer getPort() {
- return port;
- }
-
- public void setPort(Integer port) {
- this.port = port;
- }
-
- public String getDatabase() {
- return database;
- }
-
- public void setDatabase(String database) {
- this.database = database;
- }
-
- public String getUsername() {
- return username;
- }
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- public String getSchema() {
- return schema;
- }
-
- public void setSchema(String schema) {
- this.schema = schema;
- }
-
- public String getTableName() {
- return tableName;
- }
-
- public void setTableName(String tableName) {
- this.tableName = tableName;
- }
- }
-
- /**
- * Configuration for Qdrant vector database.
- */
- public static class QdrantSettings {
- private String apiKey;
- private Boolean useTls;
-
- public String getApiKey() {
- return apiKey;
- }
-
- public void setApiKey(String apiKey) {
- this.apiKey = apiKey;
- }
-
- public Boolean getUseTls() {
- return useTls;
- }
-
- public void setUseTls(Boolean useTls) {
- this.useTls = useTls;
- }
- }
+public record VectorDbConfig(
+ @NotBlank(message = "Vector database type must be specified") String type,
+ @NotBlank(message = "Connection URL must be specified") String connectionUrl,
+ @NotBlank(message = "Collection name must be specified") String collectionName,
+ @Valid ChromaSettings chroma,
+ @Valid PgVectorSettings pgvector,
+ @Valid QdrantSettings qdrant) {
+
+ /** Configuration for ChromaDB. */
+ public record ChromaSettings(String tenant, String database) {}
+
+ /** Configuration for PostgreSQL with pgvector extension. */
+ public record PgVectorSettings(
+ @NotBlank(message = "PostgreSQL host must be specified") String host,
+ @Positive(message = "PostgreSQL port must be positive") Integer port,
+ @NotBlank(message = "PostgreSQL database must be specified") String database,
+ @NotBlank(message = "PostgreSQL username must be specified") String username,
+ String password,
+ String schema,
+ @NotBlank(message = "PostgreSQL table name must be specified") String tableName) {}
+
+ /** Configuration for Qdrant vector database. */
+ public record QdrantSettings(String apiKey, Boolean useTls) {}
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/WebMvcConfiguration.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/WebMvcConfiguration.java
index 23fad9c..51be942 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/WebMvcConfiguration.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/WebMvcConfiguration.java
@@ -1,5 +1,6 @@
package br.com.arquivolivre.myjavagenie.config;
+import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
@@ -9,55 +10,53 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
-import java.io.IOException;
-
/**
- * Web MVC configuration for serving the React Chat UI.
- * Configures static resource handling and SPA routing.
+ * Web MVC configuration for serving the React Chat UI. Configures static resource handling and SPA
+ * routing.
*/
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
- /**
- * Configure resource handlers to serve static files from the React build.
- * Static files are served at root (/), while API endpoints are at /api/*.
- */
- @Override
- public void addResourceHandlers(ResourceHandlerRegistry registry) {
- registry.addResourceHandler("/**")
- .addResourceLocations("classpath:/static/")
- .resourceChain(true)
- .addResolver(new PathResourceResolver() {
- @Override
- protected Resource getResource(@NonNull String resourcePath,
- @NonNull Resource location) throws IOException {
- // Skip resource resolution for API, WebSocket, and Actuator paths
- // These should be handled by their respective handlers/controllers
- if (resourcePath.startsWith("api/") ||
- resourcePath.startsWith("ws/") ||
- resourcePath.startsWith("actuator/")) {
- return null;
- }
-
- Resource requestedResource = location.createRelative(resourcePath);
-
- // If the resource exists, return it
- if (requestedResource.exists() && requestedResource.isReadable()) {
- return requestedResource;
- }
-
- // For SPA routing: return index.html for all other non-existent paths
- // This allows React Router to handle client-side routing
- return new ClassPathResource("/static/index.html");
- }
- });
- }
-
- /**
- * Configure view controllers for root path.
- */
- @Override
- public void addViewControllers(ViewControllerRegistry registry) {
- registry.addViewController("/").setViewName("forward:/index.html");
- }
+ /**
+ * Configure resource handlers to serve static files from the React build. Static files are served
+ * at root (/), while API endpoints are at /api/*.
+ */
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ registry
+ .addResourceHandler("/**")
+ .addResourceLocations("classpath:/static/")
+ .resourceChain(true)
+ .addResolver(
+ new PathResourceResolver() {
+ @Override
+ protected Resource getResource(
+ @NonNull String resourcePath, @NonNull Resource location) throws IOException {
+ // Skip resource resolution for API, WebSocket, and Actuator paths
+ // These should be handled by their respective handlers/controllers
+ if (resourcePath.startsWith("api/")
+ || resourcePath.startsWith("ws/")
+ || resourcePath.startsWith("actuator/")) {
+ return null;
+ }
+
+ Resource requestedResource = location.createRelative(resourcePath);
+
+ // If the resource exists, return it
+ if (requestedResource.exists() && requestedResource.isReadable()) {
+ return requestedResource;
+ }
+
+ // For SPA routing: return index.html for all other non-existent paths
+ // This allows React Router to handle client-side routing
+ return new ClassPathResource("/static/index.html");
+ }
+ });
+ }
+
+ /** Configure view controllers for root path. */
+ @Override
+ public void addViewControllers(ViewControllerRegistry registry) {
+ registry.addViewController("/").setViewName("forward:/index.html");
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/config/WebSocketConfig.java b/src/main/java/br/com/arquivolivre/myjavagenie/config/WebSocketConfig.java
index 1e0f16a..bd2ebf0 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/config/WebSocketConfig.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/config/WebSocketConfig.java
@@ -6,22 +6,21 @@
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
-/**
- * Configuration for WebSocket support.
- */
+/** Configuration for WebSocket support. */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
- private final ChatWebSocketHandler chatWebSocketHandler;
+ private final ChatWebSocketHandler chatWebSocketHandler;
- public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
- this.chatWebSocketHandler = chatWebSocketHandler;
- }
+ public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
+ this.chatWebSocketHandler = chatWebSocketHandler;
+ }
- @Override
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(chatWebSocketHandler, "/ws/chat")
- .setAllowedOriginPatterns("*"); // Configure appropriately for production
- }
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ registry
+ .addHandler(chatWebSocketHandler, "/ws/chat")
+ .setAllowedOriginPatterns("*"); // Configure appropriately for production
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/controller/ChatController.java b/src/main/java/br/com/arquivolivre/myjavagenie/controller/ChatController.java
index 1417949..38ad5e1 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/controller/ChatController.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/controller/ChatController.java
@@ -5,95 +5,107 @@
import br.com.arquivolivre.myjavagenie.model.ChatResponse;
import br.com.arquivolivre.myjavagenie.model.QueryResponse;
import br.com.arquivolivre.myjavagenie.service.ChatService;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.validation.Valid;
+import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
-import java.util.List;
-
/**
- * REST controller for chat interactions.
- * Provides endpoints for querying, retrieving history, and clearing sessions.
+ * REST controller for chat interactions. Provides endpoints for querying, retrieving history, and
+ * clearing sessions.
*/
@RestController
@RequestMapping("/api/chat")
public class ChatController {
- private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
-
- private final ChatService chatService;
-
- public ChatController(ChatService chatService) {
- this.chatService = chatService;
+ private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
+
+ private final ChatService chatService;
+
+ @SuppressFBWarnings(
+ value = "EI_EXPOSE_REP2",
+ justification =
+ "chatService is a Spring-managed singleton injected by constructor, never mutated through"
+ + " this field. SpotBugs reports it only because ChatService exposes boolean-returning"
+ + " query methods (sessionExists/clearHistory) that match its collection-mutator"
+ + " heuristic; this is a documented false positive for standard dependency injection.")
+ public ChatController(ChatService chatService) {
+ this.chatService = chatService;
+ }
+
+ /**
+ * Processes a chat query.
+ *
+ * @param request the chat request containing sessionId and message
+ * @return the chat response with answer and sources
+ */
+ @PostMapping("/query")
+ public ResponseEntity
- * Note: In production, this endpoint should be secured with authentication/authorization.
+ * REST controller for handling document ingestion requests. Provides endpoint for administrators to
+ * ingest Java 25 documentation.
+ *
+ * Note: In production, this endpoint should be secured with authentication/authorization.
*/
@RestController
@RequestMapping("/api")
@Validated
public class IngestionController {
- private static final Logger logger = LoggerFactory.getLogger(IngestionController.class);
-
- private final IngestionService ingestionService;
-
- public IngestionController(IngestionService ingestionService) {
- this.ingestionService = ingestionService;
+ private static final Logger logger = LoggerFactory.getLogger(IngestionController.class);
+
+ private final IngestionService ingestionService;
+
+ /** Root directory that ingestion paths must resolve within; defaults to the process directory. */
+ private final Path ingestionRoot;
+
+ public IngestionController(
+ IngestionService ingestionService,
+ @Value("${ingestion.base-path:.}") String ingestionBasePath) {
+ this.ingestionService = ingestionService;
+ // Resolve the configured base under the process working directory. The working directory is a
+ // constant (untrusted config never reaches Paths.get/File directly), and Path#resolve keeps the
+ // result within a known root, so a configured or requested value cannot address an arbitrary
+ // filesystem location.
+ this.ingestionRoot = Paths.get("").toAbsolutePath().resolve(ingestionBasePath).normalize();
+ }
+
+ /**
+ * Ingests documents from the specified path.
+ *
+ * TODO: Add authentication/authorization (e.g., @PreAuthorize("hasRole('ADMIN')"))
+ *
+ * @param documentPath the path to the directory or file containing documents to ingest
+ * @return ResponseEntity containing the ingestion result
+ */
+ @PostMapping("/ingest")
+ public ResponseEntity
- * TODO: Add authentication/authorization (e.g., @PreAuthorize("hasRole('ADMIN')"))
- *
- * @param documentPath the path to the directory or file containing documents to ingest
- * @return ResponseEntity containing the ingestion result
- */
- @PostMapping("/ingest")
- public ResponseEntity This package contains Spring MVC controllers that expose REST endpoints for:
+ *
* All controllers follow REST best practices with proper HTTP status codes,
- * request validation, and exception handling.
+ * All controllers follow REST best practices with proper HTTP status codes, request validation,
+ * and exception handling.
*/
package br.com.arquivolivre.myjavagenie.controller;
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/CollectionNotFoundException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/CollectionNotFoundException.java
index d750897..ebf3255 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/CollectionNotFoundException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/CollectionNotFoundException.java
@@ -1,28 +1,23 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Exception thrown when a requested vector database collection does not exist.
- */
+/** Exception thrown when a requested vector database collection does not exist. */
public class CollectionNotFoundException extends VectorDbException {
- public CollectionNotFoundException(String message) {
- super(message);
- }
+ public CollectionNotFoundException(String message) {
+ super(message);
+ }
- public CollectionNotFoundException(String message, Throwable cause) {
- super(message, cause);
- }
+ public CollectionNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static CollectionNotFoundException forCollection(String collectionName) {
- return new CollectionNotFoundException(
- String.format("Vector database collection not found: %s", collectionName)
- );
- }
+ public static CollectionNotFoundException forCollection(String collectionName) {
+ return new CollectionNotFoundException(
+ String.format("Vector database collection not found: %s", collectionName));
+ }
- public static CollectionNotFoundException forCollection(String collectionName, Throwable cause) {
- return new CollectionNotFoundException(
- String.format("Vector database collection not found: %s", collectionName),
- cause
- );
- }
+ public static CollectionNotFoundException forCollection(String collectionName, Throwable cause) {
+ return new CollectionNotFoundException(
+ String.format("Vector database collection not found: %s", collectionName), cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ConfigurationException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ConfigurationException.java
index fcc9fcf..a21eedc 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ConfigurationException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ConfigurationException.java
@@ -1,19 +1,17 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Base exception for configuration related errors.
- */
+/** Base exception for configuration related errors. */
public class ConfigurationException extends RagSystemException {
- public ConfigurationException(String message) {
- super(message);
- }
+ public ConfigurationException(String message) {
+ super(message);
+ }
- public ConfigurationException(String message, Throwable cause) {
- super(message, cause);
- }
+ public ConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public ConfigurationException(Throwable cause) {
- super(cause);
- }
+ public ConfigurationException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/DocumentProcessingException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/DocumentProcessingException.java
index fdf8660..b8678d6 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/DocumentProcessingException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/DocumentProcessingException.java
@@ -1,23 +1,21 @@
package br.com.arquivolivre.myjavagenie.exception;
/**
- * Exception thrown when document processing fails during ingestion.
- * This includes errors in reading, parsing, or chunking documents.
+ * Exception thrown when document processing fails during ingestion. This includes errors in
+ * reading, parsing, or chunking documents.
*/
public class DocumentProcessingException extends IngestionException {
- public DocumentProcessingException(String message) {
- super(message);
- }
+ public DocumentProcessingException(String message) {
+ super(message);
+ }
- public DocumentProcessingException(String message, Throwable cause) {
- super(message, cause);
- }
+ public DocumentProcessingException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static DocumentProcessingException forDocument(String documentPath, Throwable cause) {
- return new DocumentProcessingException(
- String.format("Failed to process document: %s", documentPath),
- cause
- );
- }
+ public static DocumentProcessingException forDocument(String documentPath, Throwable cause) {
+ return new DocumentProcessingException(
+ String.format("Failed to process document: %s", documentPath), cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/EmbeddingGenerationException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/EmbeddingGenerationException.java
index 1f949c3..996e7f7 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/EmbeddingGenerationException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/EmbeddingGenerationException.java
@@ -1,22 +1,18 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Exception thrown when embedding generation fails during ingestion.
- */
+/** Exception thrown when embedding generation fails during ingestion. */
public class EmbeddingGenerationException extends IngestionException {
- public EmbeddingGenerationException(String message) {
- super(message);
- }
+ public EmbeddingGenerationException(String message) {
+ super(message);
+ }
- public EmbeddingGenerationException(String message, Throwable cause) {
- super(message, cause);
- }
+ public EmbeddingGenerationException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static EmbeddingGenerationException forChunk(int chunkIndex, Throwable cause) {
- return new EmbeddingGenerationException(
- String.format("Failed to generate embedding for chunk at index: %d", chunkIndex),
- cause
- );
- }
+ public static EmbeddingGenerationException forChunk(int chunkIndex, Throwable cause) {
+ return new EmbeddingGenerationException(
+ String.format("Failed to generate embedding for chunk at index: %d", chunkIndex), cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/IngestionException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/IngestionException.java
index 9d275d0..ed873a6 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/IngestionException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/IngestionException.java
@@ -1,19 +1,17 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Base exception for document ingestion related errors.
- */
+/** Base exception for document ingestion related errors. */
public class IngestionException extends RagSystemException {
- public IngestionException(String message) {
- super(message);
- }
+ public IngestionException(String message) {
+ super(message);
+ }
- public IngestionException(String message, Throwable cause) {
- super(message, cause);
- }
+ public IngestionException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public IngestionException(Throwable cause) {
- super(cause);
- }
+ public IngestionException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/InvalidConfigurationException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/InvalidConfigurationException.java
index e4d32ba..7d34275 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/InvalidConfigurationException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/InvalidConfigurationException.java
@@ -1,22 +1,21 @@
package br.com.arquivolivre.myjavagenie.exception;
/**
- * Exception thrown when configuration validation fails.
- * This indicates that the provided configuration is invalid or incomplete.
+ * Exception thrown when configuration validation fails. This indicates that the provided
+ * configuration is invalid or incomplete.
*/
public class InvalidConfigurationException extends ConfigurationException {
- public InvalidConfigurationException(String message) {
- super(message);
- }
+ public InvalidConfigurationException(String message) {
+ super(message);
+ }
- public InvalidConfigurationException(String message, Throwable cause) {
- super(message, cause);
- }
+ public InvalidConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static InvalidConfigurationException forKey(String configKey, String reason) {
- return new InvalidConfigurationException(
- String.format("Invalid configuration for '%s': %s", configKey, reason)
- );
- }
+ public static InvalidConfigurationException forKey(String configKey, String reason) {
+ return new InvalidConfigurationException(
+ String.format("Invalid configuration for '%s': %s", configKey, reason));
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/MissingConfigurationException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/MissingConfigurationException.java
index 8983e36..27eae8b 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/MissingConfigurationException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/MissingConfigurationException.java
@@ -1,28 +1,23 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Exception thrown when required configuration is missing.
- */
+/** Exception thrown when required configuration is missing. */
public class MissingConfigurationException extends ConfigurationException {
- public MissingConfigurationException(String message) {
- super(message);
- }
+ public MissingConfigurationException(String message) {
+ super(message);
+ }
- public MissingConfigurationException(String message, Throwable cause) {
- super(message, cause);
- }
+ public MissingConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static MissingConfigurationException forKey(String configKey) {
- return new MissingConfigurationException(
- String.format("Required configuration is missing: %s", configKey)
- );
- }
+ public static MissingConfigurationException forKey(String configKey) {
+ return new MissingConfigurationException(
+ String.format("Required configuration is missing: %s", configKey));
+ }
- public static MissingConfigurationException forKey(String configKey, Throwable cause) {
- return new MissingConfigurationException(
- String.format("Required configuration is missing: %s", configKey),
- cause
- );
- }
+ public static MissingConfigurationException forKey(String configKey, Throwable cause) {
+ return new MissingConfigurationException(
+ String.format("Required configuration is missing: %s", configKey), cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelException.java
index cd30e3e..ad1ec00 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelException.java
@@ -1,19 +1,17 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Base exception for language model related errors.
- */
+/** Base exception for language model related errors. */
public class ModelException extends RagSystemException {
- public ModelException(String message) {
- super(message);
- }
+ public ModelException(String message) {
+ super(message);
+ }
- public ModelException(String message, Throwable cause) {
- super(message, cause);
- }
+ public ModelException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public ModelException(Throwable cause) {
- super(cause);
- }
+ public ModelException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInitializationException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInitializationException.java
index dd86c30..4afd9a7 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInitializationException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInitializationException.java
@@ -1,23 +1,21 @@
package br.com.arquivolivre.myjavagenie.exception;
/**
- * Exception thrown when a language model fails to initialize.
- * This typically occurs during startup when the model provider cannot be configured or connected.
+ * Exception thrown when a language model fails to initialize. This typically occurs during startup
+ * when the model provider cannot be configured or connected.
*/
public class ModelInitializationException extends ModelException {
- public ModelInitializationException(String message) {
- super(message);
- }
+ public ModelInitializationException(String message) {
+ super(message);
+ }
- public ModelInitializationException(String message, Throwable cause) {
- super(message, cause);
- }
+ public ModelInitializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static ModelInitializationException forProvider(String providerName, Throwable cause) {
- return new ModelInitializationException(
- String.format("Failed to initialize language model provider: %s", providerName),
- cause
- );
- }
+ public static ModelInitializationException forProvider(String providerName, Throwable cause) {
+ return new ModelInitializationException(
+ String.format("Failed to initialize language model provider: %s", providerName), cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInvocationException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInvocationException.java
index 4b630cc..9cc0a55 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInvocationException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelInvocationException.java
@@ -1,23 +1,24 @@
package br.com.arquivolivre.myjavagenie.exception;
/**
- * Exception thrown when a language model invocation fails.
- * This occurs when the model is initialized but fails during text generation.
+ * Exception thrown when a language model invocation fails. This occurs when the model is
+ * initialized but fails during text generation.
*/
public class ModelInvocationException extends ModelException {
- public ModelInvocationException(String message) {
- super(message);
- }
+ public ModelInvocationException(String message) {
+ super(message);
+ }
- public ModelInvocationException(String message, Throwable cause) {
- super(message, cause);
- }
+ public ModelInvocationException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static ModelInvocationException forOperation(String providerName, String operation, Throwable cause) {
- return new ModelInvocationException(
- String.format("Failed to invoke %s on language model provider: %s", operation, providerName),
- cause
- );
- }
+ public static ModelInvocationException forOperation(
+ String providerName, String operation, Throwable cause) {
+ return new ModelInvocationException(
+ String.format(
+ "Failed to invoke %s on language model provider: %s", operation, providerName),
+ cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelTimeoutException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelTimeoutException.java
index 244bd74..430bb21 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelTimeoutException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/ModelTimeoutException.java
@@ -1,21 +1,18 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Exception thrown when a language model operation exceeds the configured timeout.
- */
+/** Exception thrown when a language model operation exceeds the configured timeout. */
public class ModelTimeoutException extends ModelException {
- public ModelTimeoutException(String message) {
- super(message);
- }
+ public ModelTimeoutException(String message) {
+ super(message);
+ }
- public ModelTimeoutException(String message, Throwable cause) {
- super(message, cause);
- }
+ public ModelTimeoutException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static ModelTimeoutException afterSeconds(long timeoutSeconds) {
- return new ModelTimeoutException(
- String.format("Language model operation timed out after %d seconds", timeoutSeconds)
- );
- }
+ public static ModelTimeoutException afterSeconds(long timeoutSeconds) {
+ return new ModelTimeoutException(
+ String.format("Language model operation timed out after %d seconds", timeoutSeconds));
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/RagSystemException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/RagSystemException.java
index e3ad14a..b03e004 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/RagSystemException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/RagSystemException.java
@@ -1,20 +1,20 @@
package br.com.arquivolivre.myjavagenie.exception;
/**
- * Base exception for all RAG system errors.
- * All custom exceptions in the system should extend this class.
+ * Base exception for all RAG system errors. All custom exceptions in the system should extend this
+ * class.
*/
public class RagSystemException extends RuntimeException {
- public RagSystemException(String message) {
- super(message);
- }
+ public RagSystemException(String message) {
+ super(message);
+ }
- public RagSystemException(String message, Throwable cause) {
- super(message, cause);
- }
+ public RagSystemException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public RagSystemException(Throwable cause) {
- super(cause);
- }
+ public RagSystemException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbConnectionException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbConnectionException.java
index a2a0e20..a8a5cbe 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbConnectionException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbConnectionException.java
@@ -1,23 +1,23 @@
package br.com.arquivolivre.myjavagenie.exception;
/**
- * Exception thrown when connection to the vector database fails.
- * This can occur during initialization or when the database becomes unavailable.
+ * Exception thrown when connection to the vector database fails. This can occur during
+ * initialization or when the database becomes unavailable.
*/
public class VectorDbConnectionException extends VectorDbException {
- public VectorDbConnectionException(String message) {
- super(message);
- }
+ public VectorDbConnectionException(String message) {
+ super(message);
+ }
- public VectorDbConnectionException(String message, Throwable cause) {
- super(message, cause);
- }
+ public VectorDbConnectionException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static VectorDbConnectionException forDatabase(String dbType, String connectionUrl, Throwable cause) {
- return new VectorDbConnectionException(
- String.format("Failed to connect to %s vector database at %s", dbType, connectionUrl),
- cause
- );
- }
+ public static VectorDbConnectionException forDatabase(
+ String dbType, String connectionUrl, Throwable cause) {
+ return new VectorDbConnectionException(
+ String.format("Failed to connect to %s vector database at %s", dbType, connectionUrl),
+ cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbException.java
index a88c178..c0600c6 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbException.java
@@ -1,19 +1,17 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Base exception for vector database related errors.
- */
+/** Base exception for vector database related errors. */
public class VectorDbException extends RagSystemException {
- public VectorDbException(String message) {
- super(message);
- }
+ public VectorDbException(String message) {
+ super(message);
+ }
- public VectorDbException(String message, Throwable cause) {
- super(message, cause);
- }
+ public VectorDbException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public VectorDbException(Throwable cause) {
- super(cause);
- }
+ public VectorDbException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbQueryException.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbQueryException.java
index a50f3ad..28b6d47 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbQueryException.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/VectorDbQueryException.java
@@ -1,22 +1,18 @@
package br.com.arquivolivre.myjavagenie.exception;
-/**
- * Exception thrown when a vector database query operation fails.
- */
+/** Exception thrown when a vector database query operation fails. */
public class VectorDbQueryException extends VectorDbException {
- public VectorDbQueryException(String message) {
- super(message);
- }
+ public VectorDbQueryException(String message) {
+ super(message);
+ }
- public VectorDbQueryException(String message, Throwable cause) {
- super(message, cause);
- }
+ public VectorDbQueryException(String message, Throwable cause) {
+ super(message, cause);
+ }
- public static VectorDbQueryException forOperation(String operation, Throwable cause) {
- return new VectorDbQueryException(
- String.format("Vector database query failed during operation: %s", operation),
- cause
- );
- }
+ public static VectorDbQueryException forOperation(String operation, Throwable cause) {
+ return new VectorDbQueryException(
+ String.format("Vector database query failed during operation: %s", operation), cause);
+ }
}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/exception/package-info.java b/src/main/java/br/com/arquivolivre/myjavagenie/exception/package-info.java
index 1e87ba6..81bb7ba 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/exception/package-info.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/exception/package-info.java
@@ -1,35 +1,47 @@
/**
* Exception hierarchy for the RAG system.
- *
- * All exceptions extend from {@link br.com.arquivolivre.myjavagenie.exception.RagSystemException} which is a RuntimeException.
- * The hierarchy is organized as follows:
+ *
+ * All exceptions extend from {@link
+ * br.com.arquivolivre.myjavagenie.exception.RagSystemException} which is a RuntimeException. The
+ * hierarchy is organized as follows:
+ *
*
- * This package contains the main application entry point and core configuration
- * for a Retrieval-Augmented Generation (RAG) system designed to answer questions
- * about Java 25 documentation using natural language processing.
- *
- * The system integrates:
+ *
+ * This package contains the main application entry point and core configuration for a
+ * Retrieval-Augmented Generation (RAG) system designed to answer questions about Java 25
+ * documentation using natural language processing.
+ *
+ * The system integrates:
+ *
*
- * Key features:
+ *
+ * Key features:
+ *
* Key components:
*
- * Key components: Key components:
*
- * Key components: Untrusted values (request parameters, exception messages derived from user input, etc.) can
+ * contain carriage-return/line-feed characters that would otherwise let an attacker forge or split
+ * log records (log injection). Passing such values through {@link #sanitize(Object)} replaces those
+ * control characters so each logged value stays on a single line.
+ */
+public final class LogSanitizer {
+
+ private LogSanitizer() {}
+
+ /**
+ * Returns a single-line representation of the given value with CR/LF characters replaced.
+ *
+ * @param value the value to sanitize; may be {@code null}
+ * @return the sanitized string, or {@code null} if {@code value} is {@code null}
+ */
+ public static String sanitize(Object value) {
+ if (value == null) {
+ return null;
+ }
+ return sanitize(String.valueOf(value));
+ }
+
+ /**
+ * Returns the given string with CR/LF characters replaced so it cannot span multiple log lines.
+ *
+ * @param value the string to sanitize; may be {@code null}
+ * @return the sanitized string, or {@code null} if {@code value} is {@code null}
+ */
+ public static String sanitize(String value) {
+ if (value == null) {
+ return null;
+ }
+ return value.replace('\r', '_').replace('\n', '_');
+ }
+}
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/websocket/ChatWebSocketHandler.java b/src/main/java/br/com/arquivolivre/myjavagenie/websocket/ChatWebSocketHandler.java
index 6581873..902037b 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/websocket/ChatWebSocketHandler.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/websocket/ChatWebSocketHandler.java
@@ -1,7 +1,12 @@
package br.com.arquivolivre.myjavagenie.websocket;
import br.com.arquivolivre.myjavagenie.model.QueryStatus;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@@ -9,96 +14,135 @@
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.web.util.UriComponentsBuilder;
/**
- * WebSocket handler for real-time chat updates.
- * Manages WebSocket connections and sends query status updates to clients.
+ * WebSocket handler for real-time chat updates. Manages WebSocket connections and sends query
+ * status updates to clients.
+ *
+ * Clients may register with a stable id via {@code /ws/chat?sessionId=...} (used by the chat
+ * UI). When omitted, the Spring WebSocket session id is used.
*/
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
- private static final Logger logger = LoggerFactory.getLogger(ChatWebSocketHandler.class);
+ private static final Logger logger = LoggerFactory.getLogger(ChatWebSocketHandler.class);
+ private static final String CLIENT_SESSION_ATTR = "clientSessionId";
- private final Map> getHistory(@RequestParam String sessionId) {
+ logger.info("Retrieving history for session: {}", LogSanitizer.sanitize(sessionId));
+
+ List
> getHistory(@RequestParam String sessionId) {
- logger.info("Retrieving history for session: {}", sessionId);
-
- List
- *
*
- *
- *
*/
package br.com.arquivolivre.myjavagenie.exception;
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/filter/RequestResponseLoggingFilter.java b/src/main/java/br/com/arquivolivre/myjavagenie/filter/RequestResponseLoggingFilter.java
index 6200262..98aab11 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/filter/RequestResponseLoggingFilter.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/filter/RequestResponseLoggingFilter.java
@@ -1,176 +1,165 @@
package br.com.arquivolivre.myjavagenie.filter;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Enumeration;
-
/**
- * Filter for logging HTTP requests and responses.
- * Captures request details, response status, and processing time.
+ * Filter for logging HTTP requests and responses. Captures request details, response status, and
+ * processing time.
*/
@Component
public class RequestResponseLoggingFilter implements Filter {
- private static final Logger logger = LoggerFactory.getLogger(RequestResponseLoggingFilter.class);
- private static final int MAX_PAYLOAD_LENGTH = 1000;
+ private static final Logger logger = LoggerFactory.getLogger(RequestResponseLoggingFilter.class);
+ private static final int MAX_PAYLOAD_LENGTH = 1000;
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
- if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) {
+ if (request instanceof HttpServletRequest httpRequest
+ && response instanceof HttpServletResponse httpResponse) {
- // Wrap request and response to cache content
- ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
- ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpResponse);
+ // Wrap request and response to cache content
+ ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
+ ContentCachingResponseWrapper responseWrapper =
+ new ContentCachingResponseWrapper(httpResponse);
- long startTime = System.currentTimeMillis();
+ long startTime = System.currentTimeMillis();
- try {
- // Log request
- logRequest(requestWrapper);
+ try {
+ // Log request
+ logRequest(requestWrapper);
- // Continue with the filter chain
- chain.doFilter(requestWrapper, responseWrapper);
+ // Continue with the filter chain
+ chain.doFilter(requestWrapper, responseWrapper);
- } finally {
- long duration = System.currentTimeMillis() - startTime;
+ } finally {
+ long duration = System.currentTimeMillis() - startTime;
- // Log response
- logResponse(responseWrapper, duration);
+ // Log response
+ logResponse(responseWrapper, duration);
- // Copy response content back to original response
- responseWrapper.copyBodyToResponse();
- }
- } else {
- chain.doFilter(request, response);
- }
+ // Copy response content back to original response
+ responseWrapper.copyBodyToResponse();
+ }
+ } else {
+ chain.doFilter(request, response);
}
-
- /**
- * Logs HTTP request details.
- */
- private void logRequest(ContentCachingRequestWrapper request) {
- StringBuilder logMessage = new StringBuilder();
- logMessage.append("HTTP Request: ");
- logMessage.append(request.getMethod()).append(" ");
- logMessage.append(request.getRequestURI());
-
- String queryString = request.getQueryString();
- if (queryString != null) {
- logMessage.append("?").append(queryString);
- }
-
- // Log headers (excluding sensitive ones)
- logMessage.append(" | Headers: {");
- Enumeration
- *
- *
- *
- *
- *
- *
- *
- *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
*
- *
- *
- *
- *
- *
*/
package br.com.arquivolivre.myjavagenie.repository;
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/service/ChatService.java b/src/main/java/br/com/arquivolivre/myjavagenie/service/ChatService.java
index a8fea23..dbf6331 100644
--- a/src/main/java/br/com/arquivolivre/myjavagenie/service/ChatService.java
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/service/ChatService.java
@@ -1,162 +1,181 @@
package br.com.arquivolivre.myjavagenie.service;
import br.com.arquivolivre.myjavagenie.model.*;
+import br.com.arquivolivre.myjavagenie.util.LogSanitizer;
import br.com.arquivolivre.myjavagenie.websocket.ChatWebSocketHandler;
+import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
-import java.util.List;
-
/**
- * Service for handling chat interactions.
- * Manages conversation flow and integrates with QueryService.
+ * Service for handling chat interactions. Manages conversation flow and integrates with
+ * QueryService.
*/
@Service
public class ChatService {
- private static final Logger logger = LoggerFactory.getLogger(ChatService.class);
-
- private final QueryService queryService;
- private final SessionManager sessionManager;
-
- @Autowired(required = false)
- private ChatWebSocketHandler webSocketHandler;
-
- public ChatService(QueryService queryService, SessionManager sessionManager) {
- this.queryService = queryService;
- this.sessionManager = sessionManager;
+ private static final Logger logger = LoggerFactory.getLogger(ChatService.class);
+
+ private final QueryService queryService;
+ private final SessionRegistry sessionManager;
+ private final ChatWebSocketHandler webSocketHandler;
+
+ public ChatService(
+ QueryService queryService,
+ SessionRegistry sessionManager,
+ @Nullable ChatWebSocketHandler webSocketHandler) {
+ this.queryService = queryService;
+ this.sessionManager = sessionManager;
+ this.webSocketHandler = webSocketHandler;
+ }
+
+ /**
+ * Processes a user message and generates a response.
+ *
+ * @param sessionId the session ID (null to create new session)
+ * @param message the user's message
+ * @return the query response with session information
+ */
+ public QueryResponse processMessage(String sessionId, String message) {
+ return processMessage(sessionId, message, null);
+ }
+
+ /**
+ * Processes a user message and generates a response with WebSocket status updates.
+ *
+ * @param sessionId the session ID (null to create new session)
+ * @param message the user's message
+ * @param webSocketSessionId the WebSocket session ID for status updates (optional)
+ * @return the query response with session information
+ */
+ public QueryResponse processMessage(String sessionId, String message, String webSocketSessionId) {
+ logger.info("Processing chat message for session: {}", LogSanitizer.sanitize(sessionId));
+
+ // Get or create session
+ ChatSession session = sessionManager.getOrCreateSession(sessionId);
+ // Prefer explicit WS id; otherwise use chat session id (UI connects with ?sessionId=...)
+ String statusSessionId =
+ (webSocketSessionId != null && !webSocketSessionId.isBlank())
+ ? webSocketSessionId
+ : session.getSessionId();
+
+ // Add user message to session
+ ChatMessage userMessage = new ChatMessage(ChatMessage.MessageRole.USER, message);
+ session.addMessage(userMessage);
+ logger.debug(
+ "Added user message to session {}: {}",
+ LogSanitizer.sanitize(session.getSessionId()),
+ LogSanitizer.sanitize(message));
+
+ // Send embedding status
+ sendStatusUpdate(
+ statusSessionId,
+ session.getSessionId(),
+ QueryStatus.ProcessingStage.EMBEDDING,
+ "Generating query embedding");
+
+ // Send searching status
+ sendStatusUpdate(
+ statusSessionId,
+ session.getSessionId(),
+ QueryStatus.ProcessingStage.SEARCHING,
+ "Searching for relevant documents");
+
+ // Send generating status
+ sendStatusUpdate(
+ statusSessionId,
+ session.getSessionId(),
+ QueryStatus.ProcessingStage.GENERATING,
+ "Generating response");
+
+ // Process query
+ QueryResponse response = queryService.processQuery(message);
+
+ // Add assistant response to session
+ ChatMessage assistantMessage =
+ new ChatMessage(
+ ChatMessage.MessageRole.ASSISTANT, response.getAnswer(), response.getSources());
+ session.addMessage(assistantMessage);
+ logger.debug(
+ "Added assistant response to session {}", LogSanitizer.sanitize(session.getSessionId()));
+
+ // Update response with session ID
+ QueryResponse finalResponse =
+ new QueryResponse(
+ response.getAnswer(),
+ response.getSources(),
+ response.getTokenUsage(),
+ response.getResponseTimeMs(),
+ session.getSessionId());
+
+ // Send completion status
+ if (webSocketHandler != null) {
+ ChatResponse chatResponse = ChatResponse.fromQueryResponse(finalResponse);
+ QueryStatus completionStatus = new QueryStatus(session.getSessionId(), chatResponse);
+ webSocketHandler.sendStatusUpdate(statusSessionId, completionStatus);
}
- /**
- * Processes a user message and generates a response.
- *
- * @param sessionId the session ID (null to create new session)
- * @param message the user's message
- * @return the query response with session information
- */
- public QueryResponse processMessage(String sessionId, String message) {
- return processMessage(sessionId, message, null);
+ return finalResponse;
+ }
+
+ /** Sends a status update via WebSocket if available. */
+ private void sendStatusUpdate(
+ String webSocketSessionId,
+ String chatSessionId,
+ QueryStatus.ProcessingStage stage,
+ String message) {
+ if (webSocketSessionId != null && webSocketHandler != null) {
+ QueryStatus status = new QueryStatus(chatSessionId, stage, message);
+ webSocketHandler.sendStatusUpdate(webSocketSessionId, status);
}
-
- /**
- * Processes a user message and generates a response with WebSocket status updates.
- *
- * @param sessionId the session ID (null to create new session)
- * @param message the user's message
- * @param webSocketSessionId the WebSocket session ID for status updates (optional)
- * @return the query response with session information
- */
- public QueryResponse processMessage(String sessionId, String message, String webSocketSessionId) {
- logger.info("Processing chat message for session: {}", sessionId);
-
- // Get or create session
- ChatSession session = sessionManager.getOrCreateSession(sessionId);
-
- // Add user message to session
- ChatMessage userMessage = new ChatMessage(ChatMessage.MessageRole.USER, message);
- session.addMessage(userMessage);
- logger.debug("Added user message to session {}: {}", session.getSessionId(), message);
-
- // Send embedding status
- sendStatusUpdate(webSocketSessionId, session.getSessionId(),
- QueryStatus.ProcessingStage.EMBEDDING, "Generating query embedding");
-
- // Send searching status
- sendStatusUpdate(webSocketSessionId, session.getSessionId(),
- QueryStatus.ProcessingStage.SEARCHING, "Searching for relevant documents");
-
- // Send generating status
- sendStatusUpdate(webSocketSessionId, session.getSessionId(),
- QueryStatus.ProcessingStage.GENERATING, "Generating response");
-
- // Process query
- QueryResponse response = queryService.processQuery(message);
-
- // Add assistant response to session
- ChatMessage assistantMessage = new ChatMessage(
- ChatMessage.MessageRole.ASSISTANT,
- response.getAnswer(),
- response.getSources()
- );
- session.addMessage(assistantMessage);
- logger.debug("Added assistant response to session {}", session.getSessionId());
-
- // Update response with session ID
- QueryResponse finalResponse = new QueryResponse(
- response.getAnswer(),
- response.getSources(),
- response.getTokenUsage(),
- response.getResponseTimeMs(),
- session.getSessionId()
- );
-
- // Send completion status
- if (webSocketSessionId != null && webSocketHandler != null) {
- ChatResponse chatResponse = ChatResponse.fromQueryResponse(finalResponse);
- QueryStatus completionStatus = new QueryStatus(session.getSessionId(), chatResponse);
- webSocketHandler.sendStatusUpdate(webSocketSessionId, completionStatus);
- }
-
- return finalResponse;
- }
-
- /**
- * Sends a status update via WebSocket if available.
- */
- private void sendStatusUpdate(String webSocketSessionId, String chatSessionId,
- QueryStatus.ProcessingStage stage, String message) {
- if (webSocketSessionId != null && webSocketHandler != null) {
- QueryStatus status = new QueryStatus(chatSessionId, stage, message);
- webSocketHandler.sendStatusUpdate(webSocketSessionId, status);
- }
+ }
+
+ /**
+ * Retrieves the conversation history for a session.
+ *
+ * @param sessionId the session ID
+ * @return the list of messages, or empty list if session not found
+ */
+ public List> response = embeddingModel.embedAll(segments);
+ Response
> response = embeddingModel.embedAll(segments);
- if (response == null || response.content() == null) {
- throw new EmbeddingGenerationException("Embedding model returned null response for batch");
- }
+ if (response == null || response.content() == null) {
+ throw new EmbeddingGenerationException("Embedding model returned null response for batch");
+ }
- List
- *
*/
package br.com.arquivolivre.myjavagenie.service;
diff --git a/src/main/java/br/com/arquivolivre/myjavagenie/util/LogSanitizer.java b/src/main/java/br/com/arquivolivre/myjavagenie/util/LogSanitizer.java
new file mode 100644
index 0000000..467c98a
--- /dev/null
+++ b/src/main/java/br/com/arquivolivre/myjavagenie/util/LogSanitizer.java
@@ -0,0 +1,40 @@
+package br.com.arquivolivre.myjavagenie.util;
+
+/**
+ * Utility for neutralizing CR/LF sequences in values before they are written to application logs.
+ *
+ *