parameters, @Nullable String cacheDir, @Nullable ObjectMapper mapper) {
- return switch (type) {
- case LOCAL -> new LocalCache<>(cacheDir, parameters);
- case REDIS -> new RedisCache<>(parameters, mapper);
- case REST_REDIS -> new RestRedisCache<>(parameters, mapper);
- };
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheKey.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheKey.java
deleted file mode 100644
index a68b17f3..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheKey.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-
-/**
- * Represents a key for caching operations in the LiSSA framework.
- *
- */
-public interface CacheKey {
- /**
- * Shared ObjectMapper instance for JSON serialization.
- */
- ObjectMapper MAPPER = new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true);
-
- /**
- * Converts this cache key to a JSON string representation.
- * The resulting string can be used as a unique identifier for the cached value.
- *
- * @return A JSON string representation of this cache key
- */
- default String toJsonKey() {
- try {
- return MAPPER.writeValueAsString(this);
- } catch (JsonProcessingException e) {
- throw new IllegalArgumentException("Could not serialize key", e);
- }
- }
-
- /**
- * Returns a local key for in-memory cache identification and logging purposes.
- *
- * This key is:
- *
- * - Excluded from JSON serialization (annotated with {@link com.fasterxml.jackson.annotation.JsonIgnore @JsonIgnore})
- * - Used for human-readable logging and debugging
- *
- *
- * The local key is separate from the JSON key ({@link #toJsonKey()}) because it enables custom key generation
- * strategies for special cases
- *
- * @return A string representing the local key, typically a UUID derived from the cache key's content
- */
- String localKey();
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheManager.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheManager.java
deleted file mode 100644
index ef6afd28..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheManager.java
+++ /dev/null
@@ -1,277 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import org.jspecify.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-/**
- * Manages caching operations in the LiSSA framework.
- * This class provides a centralized way to create and access caches for different purposes,
- * such as storing embeddings or chat responses. It supports both local file-based caching
- * and Redis-based caching with automatic synchronization.
- */
-public final class CacheManager {
- /**
- * The default directory name for storing cache files.
- */
- public static final String DEFAULT_CACHE_DIRECTORY = "cache";
-
- /**
- * The default cache hierarchy: LOCAL only.
- */
- private static final String DEFAULT_CACHE_HIERARCHY = "LOCAL";
-
- /**
- * The default strategy for handling cache conflicts between local and Redis caches.
- */
- private static final CacheReplacementStrategy DEFAULT_REPLACEMENT_STRATEGY = CacheReplacementStrategy.NONE;
-
- private static @Nullable CacheManager defaultInstanceManager;
- private final Path directoryOfCaches;
- private final CacheReplacementStrategy replacementStrategy;
- private final List hierarchyConfig;
- private final Map> caches = new HashMap<>();
-
- private static final Logger logger = LoggerFactory.getLogger(CacheManager.class);
-
- /**
- * Sets the cache directory for the default cache manager instance.
- * This method must be called before using the default instance.
- *
- * @param directory The path to the cache directory, or null to use the default directory
- * @throws IOException If the cache directory cannot be created
- */
- public static synchronized void setCacheDir(@Nullable String directory) throws IOException {
- defaultInstanceManager = new CacheManager(Path.of(directory == null ? DEFAULT_CACHE_DIRECTORY : directory));
- }
-
- /**
- * Reads the cache replacement strategy from environment variables.
- * This method:
- *
- * - First checks the environment variable CACHE_REPLACEMENT_STRATEGY
- * - If not found, uses the default strategy ({@link #DEFAULT_REPLACEMENT_STRATEGY})
- *
- *
- * @return The cache replacement strategy
- * @throws IllegalArgumentException If the environment variable value is set but invalid
- */
- private static CacheReplacementStrategy readCacheReplacementStrategy() {
- String strategyValue = Environment.getenv("CACHE_REPLACEMENT_STRATEGY");
- if (strategyValue == null) {
- return DEFAULT_REPLACEMENT_STRATEGY;
- }
-
- try {
- return CacheReplacementStrategy.valueOf(strategyValue.trim().toUpperCase());
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException(
- "Invalid CACHE_REPLACEMENT_STRATEGY value: " + strategyValue + ".\n"
- + Arrays.toString(CacheReplacementStrategy.values()) + " are valid options.",
- e);
- }
- }
-
- /**
- * Reads the cache hierarchy configuration from environment variables or uses the default if it's not set.
- *
- * @return The cache hierarchy configuration string
- */
- private static String readHierarchyString() {
- String hierarchyString = Environment.getenv("CACHE_HIERARCHY");
- if (hierarchyString == null) {
- return DEFAULT_CACHE_HIERARCHY;
- }
- return hierarchyString;
- }
-
- /**
- * Creates a new cache manager instance using the specified cache directory.
- * The directory will be created if it doesn't exist.
- *
- * @param cacheDir The path to the cache directory
- * @throws IOException If the cache directory cannot be created
- * @throws IllegalArgumentException If the path exists but is not a directory
- */
- public CacheManager(Path cacheDir) throws IOException {
- this(cacheDir, readCacheReplacementStrategy(), parseCacheHierarchy(readHierarchyString()));
- }
-
- /**
- * Creates a new cache manager instance with the specified cache directory, replacement strategy, and cache
- * hierarchy configuration.
- * The directory will be created if it doesn't exist.
- *
- * @param cacheDir The path to the cache directory
- * @param replacementStrategy The strategy for handling conflicts between cache layers
- * @param hierarchyConfig Non-empty list of cache types in the hierarchy order.
- * @throws IOException If the cache directory cannot be created
- * @throws IllegalArgumentException If the path exists but is not a directory
- */
- public CacheManager(Path cacheDir, CacheReplacementStrategy replacementStrategy, List hierarchyConfig)
- throws IOException {
- if (!Files.exists(cacheDir)) Files.createDirectories(cacheDir);
- if (!Files.isDirectory(cacheDir)) {
- throw new IllegalArgumentException("path is not a directory: " + cacheDir);
- }
-
- this.directoryOfCaches = Objects.requireNonNull(cacheDir);
- this.replacementStrategy = Objects.requireNonNull(replacementStrategy);
- if (hierarchyConfig.isEmpty()) {
- throw new IllegalArgumentException("Cache hierarchy configuration must contain at least one cache type");
- }
- this.hierarchyConfig = hierarchyConfig;
- }
-
- /**
- * Gets the default cache manager instance.
- * The cache directory must be set using {@link #setCacheDir(String)} before calling this method.
- *
- * @return The default cache manager instance
- * @throws IllegalStateException If the cache directory has not been set
- */
- public static CacheManager getDefaultInstance() {
- if (defaultInstanceManager == null) throw new IllegalStateException("Cache directory not set");
- return defaultInstanceManager;
- }
-
- /**
- * Gets a cache instance for the specified name.
- * This method is designed for internal use by model implementations.
- * The cache name will be sanitized by replacing colons with double underscores.
- *
- * @param origin The class origin (caller, {@code this})
- * @param parameters a list of parameters that define what makes a cache unique. E.g., the model name, temperature, and seed.
- * @param The type of cache key used in this cache
- * @return A cache instance for the specified name
- */
- public Cache getCache(Object origin, CacheParameter parameters) {
- if (origin == null || parameters == null) {
- throw new IllegalArgumentException("Origin and parameters must not be null");
- }
- String name = origin.getClass().getSimpleName() + "_" + parameters.parameters();
- return getCache(name, parameters);
- }
-
- /**
- * Gets a cache instance for the specified name and parameters.
- *
- * @param name The name of the cache
- * @param parameters The parameters that define the cache configuration
- * @return A cache instance for the specified name
- */
- private Cache getCache(String name, CacheParameter parameters) {
- name = name.replace(":", "__");
-
- if (caches.containsKey(name)) {
- @SuppressWarnings("unchecked")
- Cache cached = (Cache) caches.get(name);
- if (!cached.getCacheParameter().equals(parameters)) {
- throw new IllegalArgumentException(
- "Cache with name " + name + " already exists with different parameters");
- }
- return cached;
- }
-
- Cache cache = buildCacheHierarchy(name, parameters);
- caches.put(name, cache);
- return cache;
- }
-
- /**
- * Builds a cache hierarchy based on the configured cache types.
- * The hierarchy is read from the CACHE_HIERARCHY environment variable.
- * Caches are layered in the order specified: the first cache is the primary layer,
- * the second is the secondary layer, etc.
- * If only one cache type is specified, it is returned directly without layering.
- *
- * @param The type of cache key
- * @param cacheName The name of the cache
- * @param parameters The cache parameters
- * @return The configured cache instance
- */
- private Cache buildCacheHierarchy(String cacheName, CacheParameter parameters) {
- ObjectMapper mapper = new ObjectMapper();
- String cacheFilePath = directoryOfCaches.resolve(cacheName + ".json").toString();
- List> createdCaches = new ArrayList<>();
- for (CacheType cacheType : hierarchyConfig) {
- Cache cache = Cache.createByType(cacheType, parameters, cacheFilePath, mapper);
- createdCaches.add(cache);
- logger.debug("Created cache type: {}", cacheType);
- }
-
- Cache layeredCache = createdCaches.getFirst();
- for (int i = 1; i < createdCaches.size(); i++) {
- layeredCache = new HierarchicalCache<>(parameters, layeredCache, createdCaches.get(i), replacementStrategy);
- }
- return layeredCache;
- }
-
- /**
- * Parses the cache hierarchy configuration string into a list of cache types.
- * The input should be a comma-separated list of cache types (case-insensitive).
- * Supports quoted strings to handle spaces: e.g., 'REDIS, LOCAL' or "LOCAL, REDIS".
- *
- * @param hierarchyConfig The hierarchy configuration string
- * @return A list of cache types in order
- * @throws IllegalArgumentException If the configuration is empty or invalid
- */
- private static List parseCacheHierarchy(String hierarchyConfig) {
- String[] types = hierarchyConfig.replace("'", "").replace('"', ' ').split(",");
- List cacheTypes = new ArrayList<>();
-
- for (String type : types) {
- String trimmed = type.trim();
- if (trimmed.isEmpty()) {
- throw new IllegalArgumentException("Cache hierarchy contains empty cache type");
- }
- try {
- cacheTypes.add(CacheType.valueOf(trimmed.toUpperCase()));
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException(
- "Invalid CACHE_HIERARCHY value: " + trimmed + ".\n" + Arrays.toString(CacheType.values())
- + " are valid options.",
- e);
- }
- }
- return cacheTypes;
- }
-
- /**
- * Flushes all caches managed by this cache manager.
- * This ensures that all pending changes are written to disk.
- */
- public void flush() {
- for (Cache> cache : caches.values()) {
- cache.flush();
- }
- }
-
- /**
- * Resets the default cache manager instance.
- * This method is intended for testing purposes only to allow clean state between tests.
- * After calling this method, {@link #setCacheDir(String)}
- * must be called again before using the default instance.
- */
- static synchronized void resetDefaultInstance() {
- if (defaultInstanceManager != null) {
- defaultInstanceManager.flush();
- }
- defaultInstanceManager = null;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheParameter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheParameter.java
deleted file mode 100644
index 806962e9..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheParameter.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-/**
- * Interface for cache parameter implementations that define how cache keys are created and configured.
- * Implementations specify the parameters that make a cache unique (e.g., model name, seed, temperature)
- * and provide factory methods for creating cache keys.
- *
- * @param The type of cache key this parameter creates
- */
-public interface CacheParameter {
- /**
- * Provides a unique string based on the actual cache parameters.
- * This string is used for the file name of LocalCache and must uniquely identify the cache configuration.
- *
- * @return A unique string based on the cache parameters
- */
- String parameters();
-
- /**
- * Creates a cache key based on the content and the cache parameters.
- * The created key combines the cache configuration with the content to be cached.
- *
- * @param content The content to create the cache key for
- * @return The created cache key
- */
- K createCacheKey(String content);
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategy.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategy.java
deleted file mode 100644
index be9d4f33..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategy.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.util.Objects;
-
-import org.jspecify.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Defines strategies for handling cache value replacement when conflicts occur between cache layers.
- * A conflict occurs when the same key exists in both caches but with different values.
- */
-public enum CacheReplacementStrategy {
- /**
- * Does not replace conflicting values - leaves both cache values as they are.
- * If any value is null while the other is present the missing value is backfilled.
- * The primary value will be returned when reading.
- */
- NONE,
-
- /**
- * Does not replace conflicting values - leaves both cache values as they are.
- * If a conflict is detected an exception will be thrown.
- */
- ERROR {
- /**
- * Throws an exception when a conflict is detected between the two caches.
- */
- @Override
- public @Nullable T resolve(
- String key,
- @Nullable T primaryValue,
- Cache primaryCache,
- @Nullable T secondaryValue,
- Cache secondaryCache) {
- if (primaryValue != null && secondaryValue != null && !Objects.deepEquals(primaryValue, secondaryValue)) {
- logger.error(
- "Cache inconsistency detected for key {}, values: {} (primary cache), {} (secondary cache)",
- key,
- primaryValue,
- secondaryValue);
- throw new IllegalStateException("Cache inconsistency detected for key " + key);
- }
- return super.resolve(key, primaryValue, primaryCache, secondaryValue, secondaryCache);
- }
-
- /**
- * Throws an exception when a conflict is detected between the two caches.
- *
- * @deprecated This method exposes internal cache key handling and should not be used in general code.
- */
- @Override
- @Deprecated(forRemoval = false)
- @Nullable T resolveViaInternalKey(
- K key,
- @Nullable T primaryValue,
- Cache primaryCache,
- @Nullable T secondaryValue,
- Cache secondaryCache) {
- if (primaryValue != null && secondaryValue != null && !Objects.deepEquals(primaryValue, secondaryValue)) {
- logger.error(
- "Cache inconsistency detected for key {}, values: {} (primary cache), {} (secondary cache)",
- key,
- primaryValue,
- secondaryValue);
- throw new IllegalStateException("Cache inconsistency detected for key " + key);
- }
- return super.resolveViaInternalKey(key, primaryValue, primaryCache, secondaryValue, secondaryCache);
- }
- },
-
- /**
- * Replaces the conflicting value in the secondary cache with the value from the primary cache.
- */
- OVERWRITE {
- /**
- * Overwrites the secondary cache value with the primary cache value in case of a conflict, and returns the primary cache value.
- */
- @Override
- public @Nullable T resolve(
- String key,
- @Nullable T primaryValue,
- Cache primaryCache,
- @Nullable T secondaryValue,
- Cache secondaryCache) {
- if (primaryValue != null && secondaryValue != null && !Objects.deepEquals(primaryValue, secondaryValue)) {
- logger.warn(
- "Cache inconsistency detected for key {}, overwriting secondary cache value with primary cache value: {} -> {}",
- key,
- secondaryValue,
- primaryValue);
- secondaryCache.put(key, primaryValue);
- return primaryValue;
- }
- return super.resolve(key, primaryValue, primaryCache, secondaryValue, secondaryCache);
- }
-
- /**
- * Overwrites the secondary cache value with the primary cache value in case of a conflict, and returns the primary cache value.
- *
- * @deprecated This method exposes internal cache key handling and should not be used in general code.
- */
- @Override
- @Deprecated(forRemoval = false)
- @Nullable T resolveViaInternalKey(
- K key,
- @Nullable T primaryValue,
- Cache primaryCache,
- @Nullable T secondaryValue,
- Cache secondaryCache) {
- if (primaryValue != null && secondaryValue != null && !Objects.deepEquals(primaryValue, secondaryValue)) {
- logger.warn(
- "Cache inconsistency detected for key {}, overwriting secondary cache value with primary cache value: {} -> {}",
- key,
- secondaryValue,
- primaryValue);
- secondaryCache.putViaInternalKey(key, primaryValue);
- return primaryValue;
- }
- return super.resolveViaInternalKey(key, primaryValue, primaryCache, secondaryValue, secondaryCache);
- }
- };
-
- private static final Logger logger = LoggerFactory.getLogger(CacheReplacementStrategy.class);
-
- /**
- * Resolves a conflict between two caches by applying the appropriate replacement strategy.
- * If a value is null in one cache but not the other, it will be copied to the cache where it is missing.
- *
- * The default implementation does not perform any replacement and simply returns the primary value.
- *
- * @param The type of cache key used in both caches
- * @param The type of the cache values
- * @param key The cache key where the conflict occurred
- * @param primaryValue The value of the primary cache
- * @param primaryCache The primary cache where the value was found
- * @param secondaryValue The value of the secondary cache
- * @param secondaryCache The secondary cache where the value was found
- *
- * @return The resolved cache value to be used (may be null)
- */
- public @Nullable T resolve(
- String key,
- @Nullable T primaryValue,
- Cache primaryCache,
- @Nullable T secondaryValue,
- Cache secondaryCache) {
- if (primaryValue == null && secondaryValue != null) {
- primaryCache.put(key, secondaryValue);
- return secondaryValue;
- }
- if (primaryValue != null && secondaryValue == null) {
- secondaryCache.put(key, primaryValue);
- return primaryValue;
- }
- return primaryValue;
- }
-
- /**
- * Resolves a conflict between two caches by applying the appropriate replacement strategy.
- * If a value is null in one cache but not the other, it will be copied to the cache where it is missing.
- *
- * The default implementation does not perform any replacement and simply returns the primary value.
- *
- * @param The type of cache key used in both caches
- * @param The type of the cache values
- * @param key The cache key where the conflict occurred
- * @param primaryValue The value of the primary cache
- * @param primaryCache The primary cache where the value was found
- * @param secondaryValue The value of the secondary cache
- * @param secondaryCache The secondary cache where the value was found
- *
- * @return The resolved cache value to be used (may be null)
- * @deprecated This method exposes internal cache key handling and should not be used in general code.
- */
- @Deprecated(forRemoval = false)
- @Nullable T resolveViaInternalKey(
- K key,
- @Nullable T primaryValue,
- Cache primaryCache,
- @Nullable T secondaryValue,
- Cache secondaryCache) {
- if (primaryValue == null && secondaryValue != null) {
- primaryCache.putViaInternalKey(key, secondaryValue);
- return secondaryValue;
- }
- if (primaryValue != null && secondaryValue == null) {
- secondaryCache.putViaInternalKey(key, primaryValue);
- }
- return primaryValue;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheType.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheType.java
deleted file mode 100644
index 74e3be0f..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheType.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-/**
- * Enum representing the types of caches supported by the system.
- */
-public enum CacheType {
- /**
- * File based local cache
- */
- LOCAL,
- /**
- * Redis based local docker container for caching
- */
- REDIS,
- /**
- * Remote Redis instance accessible via a REST API
- */
- REST_REDIS
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCache.java
deleted file mode 100644
index 0b8e9102..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCache.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.util.Objects;
-
-import org.jspecify.annotations.Nullable;
-
-/**
- * Implements a hierarchical cache that composes multiple cache implementations.
- * This class manages synchronization and conflict resolution between multiple cache layers
- * (e.g., Redis and local file cache), providing a unified view across the cache hierarchy.
- *
- * The cache hierarchy operates as follows:
- * 1. Attempts to retrieve/store values in the primary cache
- * 2. Falls back to secondary cache if missing in the primary
- * 3. Automatically synchronizes values between layers when needed
- * 4. Applies conflict resolution strategy when values differ between layers
- *
- * @param The type of cache key used in this cache
- */
-class HierarchicalCache implements Cache {
-
- private final CacheParameter cacheParameter;
-
- /**
- * Primary cache in the hierarchy (typically Redis).
- */
- private final Cache primaryCache;
-
- /**
- * Secondary cache in the hierarchy (typically local file cache).
- */
- private final Cache secondaryCache;
-
- /**
- * Strategy for resolving conflicts between cache layers.
- */
- private final CacheReplacementStrategy conflictResolution;
-
- /**
- * Creates a new hierarchical cache instance.
- *
- * @param cacheParameter The cache parameter configuration
- * @param primaryCache The primary cache (e.g., Redis)
- * @param secondaryCache The secondary cache (e.g., local file)
- * @param conflictResolution Strategy for resolving conflicts between cache layers
- */
- HierarchicalCache(
- CacheParameter cacheParameter,
- Cache primaryCache,
- Cache secondaryCache,
- CacheReplacementStrategy conflictResolution) {
- this.cacheParameter = Objects.requireNonNull(cacheParameter);
- this.primaryCache = Objects.requireNonNull(primaryCache);
- this.secondaryCache = Objects.requireNonNull(secondaryCache);
- this.conflictResolution = Objects.requireNonNull(conflictResolution);
- }
-
- @Override
- public synchronized @Nullable T get(String key, Class clazz) {
- T primaryValue = primaryCache.get(key, clazz);
- T secondaryValue = secondaryCache.get(key, clazz);
- return conflictResolution.resolve(key, primaryValue, primaryCache, secondaryValue, secondaryCache);
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public synchronized @Nullable T getViaInternalKey(K key, Class clazz) {
- T primaryValue = primaryCache.getViaInternalKey(key, clazz);
- T secondaryValue = secondaryCache.getViaInternalKey(key, clazz);
- return conflictResolution.resolveViaInternalKey(
- key, primaryValue, primaryCache, secondaryValue, secondaryCache);
- }
-
- @Override
- public synchronized void put(String key, String value) {
- primaryCache.put(key, value);
- secondaryCache.put(key, value);
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public synchronized void putViaInternalKey(K key, T value) {
- primaryCache.putViaInternalKey(key, value);
- secondaryCache.putViaInternalKey(key, value);
- }
-
- @Override
- public synchronized void put(String key, T value) {
- primaryCache.put(key, value);
- secondaryCache.put(key, value);
- }
-
- @Override
- public void flush() {
- primaryCache.flush();
- secondaryCache.flush();
- }
-
- @Override
- public boolean containsKey(String key) {
- if (primaryCache.containsKey(key)) {
- return true;
- }
- return secondaryCache.containsKey(key);
- }
-
- @Override
- public CacheParameter getCacheParameter() {
- return cacheParameter;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LargeLanguageModelCacheMode.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LargeLanguageModelCacheMode.java
deleted file mode 100644
index 98c1d9e0..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LargeLanguageModelCacheMode.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-/**
- * Defines the possible modes of operation that can be cached.
- */
-public enum LargeLanguageModelCacheMode {
- /**
- * LargeLanguageModelCacheMode for caching embedding generation operations.
- */
- EMBEDDING,
-
- /**
- * LargeLanguageModelCacheMode for caching chat-based operations.
- */
- CHAT
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LocalCache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LocalCache.java
deleted file mode 100644
index 8abce5aa..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LocalCache.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
-import java.nio.file.StandardCopyOption;
-import java.util.*;
-
-import org.jspecify.annotations.Nullable;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * Implements a local file-based cache for storing key-value pairs.
- * This class provides a thread-safe implementation of a cache that persists its contents
- * to a JSON file. It includes automatic flushing of changes when a certain threshold
- * of modifications is reached.
- *
- * @param The type of cache key used in this cache
- */
-class LocalCache implements Cache {
-
- private final ObjectMapper mapper;
-
- /**
- * Maximum number of modifications before automatic flush.
- */
- private static final int MAX_DIRTY = 50;
-
- private final CacheParameter cacheParameter;
-
- /**
- * Counter for unflushed modifications.
- */
- private int dirty = 0;
-
- private final File cacheFile;
-
- /**
- * In-memory cache storage.
- */
- private Map cache = new HashMap<>();
-
- /**
- * Creates a new local cache instance.
- * The cache will be initialized from the specified file if it exists,
- * or a new file will be created.
- *
- * @param cacheFile The path to the cache file
- * @param cacheParameter The cache parameter configuration
- */
- LocalCache(String cacheFile, CacheParameter cacheParameter) {
- this.cacheParameter = Objects.requireNonNull(cacheParameter);
- this.cacheFile = new File(Objects.requireNonNull(cacheFile));
- mapper = new ObjectMapper();
- createLocalStore();
- }
-
- /**
- * Checks if the cache is ready for use.
- * This method ensures that the cache file exists and is accessible.
- *
- * @return true if the cache is ready, false otherwise
- * @throws UncheckedIOException If there are issues accessing the cache file
- */
- public boolean isReady() {
- try {
- return cacheFile.exists() || cacheFile.createNewFile();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- /**
- * Initializes the local cache store.
- * If the cache file exists and is not empty, its contents are loaded into memory.
- * If the file is empty, it is deleted to ensure a clean state.
- *
- * @throws IllegalArgumentException If the cache file cannot be read
- */
- private void createLocalStore() {
- if (cacheFile.exists()) {
- try {
- if (Files.readString(cacheFile.toPath()).isBlank()) {
- cacheFile.delete();
- } else {
- cache = mapper.readValue(cacheFile, new TypeReference<>() {});
- }
- } catch (IOException e) {
- throw new IllegalArgumentException("Could not read cache file (" + cacheFile.getName() + ")", e);
- }
- }
- }
-
- /**
- * Writes the current cache contents to disk.
- * This method uses a temporary file to ensure atomic writes and prevent data corruption.
- * The dirty counter is reset after a successful write.
- *
- * @throws IllegalArgumentException If the cache file cannot be written
- */
- public synchronized void write() {
- if (dirty == 0) {
- return;
- }
-
- try {
- File tempFile = new File(cacheFile.getAbsolutePath() + ".tmp.json");
- mapper.writeValue(tempFile, cache);
- Files.copy(tempFile.toPath(), cacheFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
- Files.delete(tempFile.toPath());
- dirty = 0;
- } catch (IOException e) {
- throw new IllegalArgumentException("Could not write cache file", e);
- }
- }
-
- @Override
- public synchronized @Nullable T get(String key, Class clazz) {
- K cacheKey = cacheParameter.createCacheKey(key);
- String jsonData = cache.get(cacheKey.localKey());
- return Cache.convert(jsonData, clazz, mapper);
- }
-
- /**
- * Retrieves a value from the cache.
- *
- * @param key The cache key to look up
- * @return The cached value, or null if not found
- * @deprecated This method exposes internal cache key handling and should not be used in general code.
- */
- @Override
- @Deprecated(forRemoval = false)
- public synchronized @Nullable T getViaInternalKey(K key, Class clazz) {
- String jsonData = cache.get(key.localKey());
- return Cache.convert(jsonData, clazz, mapper);
- }
-
- @Override
- public synchronized void put(String key, String value) {
- K cacheKey = cacheParameter.createCacheKey(key);
- putViaInternalKey(cacheKey, value);
- }
-
- /**
- * Stores a value in the cache.
- * If the value is different from the existing value (if any), the dirty counter is incremented.
- * If the dirty counter exceeds the maximum threshold, the cache is automatically flushed to disk.
- *
- * @param cacheKey The cache key to store the value under
- * @param value The value to store
- * @deprecated This method exposes internal cache key handling and should not be used in general code.
- */
- @Override
- @Deprecated(forRemoval = false)
- public synchronized void putViaInternalKey(K cacheKey, T value) {
- String jsonValue;
- try {
- jsonValue = mapper.writeValueAsString(Objects.requireNonNull(value));
- } catch (JsonProcessingException e) {
- throw new IllegalArgumentException("Could not serialize object", e);
- }
- String old = cache.put(cacheKey.localKey(), jsonValue);
- if (old == null || !old.equals(jsonValue)) {
- dirty++;
- }
-
- if (dirty > MAX_DIRTY) {
- write();
- }
- }
-
- @Override
- public synchronized void put(String key, T value) {
- K cacheKey = cacheParameter.createCacheKey(key);
- putViaInternalKey(cacheKey, value);
- }
-
- @Override
- public void flush() {
- write();
- }
-
- /**
- * Returns true if and only if this map contains a mapping for a key
- *
- * @param key The cache key to look up
- * @return true if this map contains a mapping for the specified key
- */
- @Override
- public synchronized boolean containsKey(String key) {
- K cacheKey = cacheParameter.createCacheKey(key);
- return cache.containsKey(cacheKey.localKey());
- }
-
- @Override
- public CacheParameter getCacheParameter() {
- return this.cacheParameter;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java
deleted file mode 100644
index c76a4802..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.util.Objects;
-
-import redis.clients.jedis.UnifiedJedis;
-
-/**
- * Adapter class that wraps a Jedis client to conform to the UnifiedRedisClient interface.
- */
-/*package-private*/ class RedisAdapter implements UnifiedRedisClient {
-
- private final UnifiedJedis jedis;
-
- /**
- * Creates a new RedisAdapter instance with the given Jedis client.
- *
- * @param jedis The Jedis client to wrap
- */
- /*package-private*/ RedisAdapter(UnifiedJedis jedis) {
- this.jedis = Objects.requireNonNull(jedis);
- }
-
- /**
- * Pings the Redis server to check if it is available.
- *
- * @return true if the server responds, false otherwise.
- */
- @Override
- public boolean ping() {
- return jedis.ping().equals("PONG");
- }
-
- /**
- * Checks if a key exists in the Redis cache.
- *
- * @param key the key to check for existence
- * @return true if the key exists, false otherwise.
- */
- @Override
- public boolean exists(String key) {
- return jedis.exists(key);
- }
-
- /**
- * Retrieves the value of a field in a hash stored at key.
- *
- * @param key The key of the hash
- * @param field The field whose value is to be retrieved
- * @return Value for the field. If the key or field does not exist, null is returned.
- */
- @Override
- public String hget(String key, String field) {
- return jedis.hget(key, field);
- }
-
- /**
- * Sets the value of a field in a hash stored at a key
- *
- * @param key The key of the hash
- * @param field The field whose value is to be set
- * @param value The value to be set
- * @return The number of added fields
- */
- @Override
- public long hset(String key, String field, String value) {
- return jedis.hset(key, field, value);
- }
-
- /**
- * Shuts down the connection to the jedis instance.
- */
- @Override
- public void close() {
- jedis.close();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java
deleted file mode 100644
index 2770e4d6..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.time.Instant;
-import java.util.*;
-
-import org.jspecify.annotations.Nullable;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-import redis.clients.jedis.RedisClient;
-
-/**
- * Implements a Redis-based cache for storing and retrieving values. For multi-layer caching with
- * synchronization and conflict resolution, use {@link HierarchicalCache}.
- *
- * The cache will fail to initialize if Redis is unavailable.
- *
- * @param The type of cache key used in this cache
- */
-class RedisCache implements Cache {
-
- private final CacheParameter cacheParameter;
- private final ObjectMapper mapper;
-
- /**
- * Redis client instance.
- */
- private final UnifiedRedisClient redis;
-
- /**
- * Creates a new Redis cache instance.
- * This constructor will throw an exception if Redis is unavailable.
- *
- * @param cacheParameter The cache parameter configuration
- * @param mapper The ObjectMapper for JSON operations
- * @throws IllegalStateException If Redis connection cannot be established
- */
- RedisCache(CacheParameter cacheParameter, ObjectMapper mapper) {
- this(cacheParameter, mapper, createRedisConnection());
- }
-
- /**
- * Creates a Redis Cache instance with a custom redis connection
- *
- * @param cacheParameter The cache parameter configuration
- * @param mapper The ObjectMapper for JSON operations
- * @param redis The connected redis instance
- */
- protected RedisCache(CacheParameter cacheParameter, ObjectMapper mapper, UnifiedRedisClient redis) {
- this.cacheParameter = Objects.requireNonNull(cacheParameter);
- this.mapper = Objects.requireNonNull(mapper);
- this.redis = Objects.requireNonNull(redis);
- }
-
- @Override
- public void flush() {
- // Redis doesn't require manual flushing
- }
-
- @Override
- public boolean containsKey(String key) {
- K cacheKey = cacheParameter.createCacheKey(key);
- return redis.exists(cacheKey.toJsonKey());
- }
-
- /**
- * Establishes a connection to the Redis server.
- * The Redis URL can be configured through the REDIS_URL environment variable.
- *
- * @throws IllegalStateException if Redis connection could not be established
- */
- private static RedisAdapter createRedisConnection() {
- String redisUrl = "redis://localhost:6379";
- if (Environment.getenv("REDIS_URL") != null) {
- redisUrl = Environment.getenv("REDIS_URL");
- }
- RedisAdapter redis = new RedisAdapter(RedisClient.create(redisUrl));
- // Check if connection is working
- if (!redis.ping()) {
- redis.close();
- throw new IllegalStateException("Could not connect to Redis. Make sure the container is up and running.");
- }
- return redis;
- }
-
- /**
- * Retrieves a value from the cache and deserializes it to the specified type.
- *
- * @param The type to deserialize the value to
- * @param key The cache key to look up
- * @param clazz The class of the type to deserialize to
- * @return The deserialized value, or null if not found
- */
- @Override
- public synchronized @Nullable T get(String key, Class clazz) {
- K cacheKey = cacheParameter.createCacheKey(key);
- String jsonData = redis.hget(cacheKey.toJsonKey(), "data");
- return Cache.convert(jsonData, clazz, mapper);
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public synchronized @Nullable T getViaInternalKey(K cacheKey, Class clazz) {
- String jsonData = redis.hget(cacheKey.toJsonKey(), "data");
- return Cache.convert(jsonData, clazz, mapper);
- }
-
- /**
- * Stores a string value in the cache.
- * When storing in Redis, a timestamp is also recorded.
- *
- * @param key The cache key to store the value under
- * @param value The string value to store
- */
- @Override
- public synchronized void put(String key, String value) {
- K cacheKey = cacheParameter.createCacheKey(key);
- String jsonKey = cacheKey.toJsonKey();
- redis.hset(jsonKey, "data", value);
- redis.hset(jsonKey, "timestamp", String.valueOf(Instant.now().getEpochSecond()));
- }
-
- /**
- * Stores an object value in the cache.
- * The object is serialized to JSON before storage.
- *
- * @param The type of the value to store
- * @param key The cache key to store the value under
- * @param value The object value to store
- * @throws IllegalArgumentException If the object cannot be serialized to JSON
- * @throws NullPointerException If value is null
- */
- @Override
- public synchronized void put(String key, T value) {
- try {
- put(key, mapper.writeValueAsString(Objects.requireNonNull(value)));
- } catch (JsonProcessingException e) {
- throw new IllegalArgumentException("Could not serialize object", e);
- }
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public synchronized void putViaInternalKey(K key, T value) {
- String data;
- try {
- data = mapper.writeValueAsString(Objects.requireNonNull(value));
- } catch (JsonProcessingException e) {
- throw new IllegalArgumentException("Could not serialize object", e);
- }
- String jsonKey = key.toJsonKey();
- redis.hset(jsonKey, "data", data);
- redis.hset(jsonKey, "timestamp", String.valueOf(Instant.now().getEpochSecond()));
- }
-
- @Override
- public CacheParameter getCacheParameter() {
- return this.cacheParameter;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java
deleted file mode 100644
index 8a1bf884..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import java.util.Objects;
-
-import org.fuchss.restredis.client.Client;
-
-/**
- * Adapter that wraps a {@link Client REST Redis client} and exposes it through
- * the {@link UnifiedRedisClient} interface. All method calls are delegated directly to the underlying
- * {@link Client} instance.
- *
- * @see UnifiedRedisClient
- */
-/*package-private*/ class RestRedisAdapter implements UnifiedRedisClient {
-
- /** The underlying REST-based Redis client to which all calls are delegated. */
- private final Client restRedisClient;
-
- /**
- * Constructs a new {@code RestRedisAdapter} wrapping the given client.
- *
- * @param restRedisClient the REST Redis client to delegate to. Must not be {@code null}
- */
- /*package-private*/ RestRedisAdapter(Client restRedisClient) {
- this.restRedisClient = Objects.requireNonNull(restRedisClient);
- }
-
- /**
- * Sends a {@code PING} command to the Redis server.
- *
- * @return {@code true} if the server responds successfully. {@code false} otherwise
- */
- @Override
- public boolean ping() {
- return restRedisClient.ping();
- }
-
- /**
- * Checks whether the given key exists in Redis.
- *
- * @param key the key to look up
- * @return {@code true} if the key exists. {@code false} otherwise
- */
- @Override
- public boolean exists(String key) {
- return restRedisClient.exists(key);
- }
-
- /**
- * Retrieves the value of a field within a Redis hash.
- *
- * @param key the hash key.
- * @param field the field within the hash.
- * @return the field's value, or {@code null} if the key or field does not exist
- */
- @Override
- public String hget(String key, String field) {
- return restRedisClient.hget(key, field);
- }
-
- /**
- * Sets a field-value pair within a Redis hash.
- *
- * @param key the hash key.
- * @param field the field to set within the hash.
- * @param value the value to store.
- * @return the number of fields that were added
- */
- @Override
- public long hset(String key, String field, String value) {
- return restRedisClient.hset(key, field, value);
- }
-
- /**
- * Closes the underlying REST Redis client and releases any held resources.
- *
- * After this method returns the adapter must not be used further.
- */
- @Override
- public void close() {
- restRedisClient.close();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java
deleted file mode 100644
index 090b5f54..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import org.fuchss.restredis.client.Client;
-import org.fuchss.restredis.client.ClientConfiguration;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-/**
- * Implements a Redis-based cache for storing and retrieving values using a REST interface.
- *
- * @param The type of cache key used in this cache
- */
-/*package-private*/ class RestRedisCache extends RedisCache {
-
- /**
- * Creates a new Rest Redis cache instance.
- * This constructor will throw an exception if Rest Redis is unavailable.
- *
- * @param cacheParameter The cache parameter configuration
- * @param mapper The ObjectMapper for JSON operations
- * @throws IllegalArgumentException If Redis connection cannot be established
- */
- /*package-private*/ RestRedisCache(CacheParameter cacheParameter, ObjectMapper mapper) {
- super(cacheParameter, mapper, createRedisConnection());
- }
-
- /**
- * Initiates the REST Redis connection using environment variables for configuration. The following environment variables are used:
- *
- * - {@code REST_REDIS_URI}: The URI of the REST Redis server (default: {@code http://localhost:8080})
- * - {@code REST_REDIS_USERNAME}: The username for authentication (optional)
- * - {@code REST_REDIS_PASSWORD}: The password for authentication (optional)
- *
- */
- private static UnifiedRedisClient createRedisConnection() {
- String restRedisUri = "http://localhost:8080";
- String restRedisUriEnv = Environment.getenv("REST_REDIS_URI");
- if (restRedisUriEnv != null) {
- restRedisUri = restRedisUriEnv;
- }
- String restRedisUsername = Environment.getenv("REST_REDIS_USERNAME");
- if (restRedisUsername != null && restRedisUsername.isBlank()) {
- restRedisUsername = null;
- }
- String restRedisPassword = Environment.getenv("REST_REDIS_PASSWORD");
- if (restRedisPassword != null && restRedisPassword.isBlank()) {
- restRedisPassword = null;
- }
-
- ClientConfiguration config = new ClientConfiguration(restRedisUri, restRedisUsername, restRedisPassword);
- UnifiedRedisClient redis = new RestRedisAdapter(new Client(config));
-
- // Check if connection is working
- if (!redis.ping()) {
- redis.close();
- throw new IllegalStateException("Could not connect to Redis at " + restRedisUri);
- }
-
- return redis;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java
deleted file mode 100644
index fdb4b7ee..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-/**
- * A unified abstraction over a Redis client, providing a minimal set of
- * operations used for caching within the application.
- *
- * Instances should be closed after use to release any held resources.
- */
-public interface UnifiedRedisClient extends AutoCloseable {
-
- /**
- * Sends a {@code PING} command to the Redis server.
- *
- * @return {@code true} if the server responds successfully, {@code false} otherwise.
- */
- boolean ping();
-
- /**
- * Checks whether the given key exists in Redis.
- *
- * @param key the key to look up.
- * @return {@code true} if the key exists, {@code false} otherwise.
- */
- boolean exists(String key);
-
- /**
- * Retrieves the value of a field within a Redis hash.
- *
- * @param key the hash key.
- * @param field the field within the hash.
- * @return the field's value, or {@code null} if the key or field does not exist.
- */
- String hget(String key, String field);
-
- /**
- * Sets a field-value pair within a Redis hash.
- *
- * @param key the hash key.
- * @param field the field to set within the hash.
- * @param value the value to store.
- * @return the number of fields that were added (not updated).
- */
- long hset(String key, String field, String value);
-
- /**
- * Closes the client and releases any held resources.
- *
- *
After this method returns the client must not be used further.
- */
- void close();
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheKey.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheKey.java
deleted file mode 100644
index 01f7fcd9..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheKey.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache.classifier;
-
-import java.util.Objects;
-
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheKey;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.LargeLanguageModelCacheMode;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
-
-/**
- * Represents a key for classification caching operations in the LiSSA framework.
- * This class is used to uniquely identify cached values based on various parameters
- * such as the model used, seed value, operation mode, and content.
- *
- * The key can be serialized to JSON for storage and retrieval from the cache.
- */
-@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public final class ClassifierCacheKey implements CacheKey {
- private final String model;
- private final int seed;
- private final double temperature;
- private final LargeLanguageModelCacheMode mode;
- private final String content;
-
- @JsonIgnore
- private final String localKey;
-
- /**
- * Creates a new classifier cache key with the specified parameters.
- *
- * @param model The identifier of the model used for the cached operation
- * @param seed The seed value used for randomization in the cached operation
- * @param temperature The temperature setting used in the cached operation
- * @param mode The mode of operation that was cached (classification for backward compatibility)
- * @param content The content that was processed in the cached operation
- * @param localKey A local key for additional identification, not included in JSON serialization
- */
- private ClassifierCacheKey(
- String model,
- int seed,
- double temperature,
- LargeLanguageModelCacheMode mode,
- String content,
- String localKey) {
- this.model = model;
- this.seed = seed;
- this.temperature = temperature;
- this.mode = mode;
- this.content = content;
- this.localKey = localKey;
- }
-
- /**
- * Creates a classifier cache key from the given cache parameter and content.
- * This is the preferred way to create cache keys.
- *
- * @param cacheParameter The cache parameter containing model configuration
- * @param content The content to be cached
- * @return A new classifier cache key
- */
- static ClassifierCacheKey of(ClassifierCacheParameter cacheParameter, String content) {
- return new ClassifierCacheKey(
- cacheParameter.modelName(),
- cacheParameter.seed(),
- cacheParameter.temperature(),
- LargeLanguageModelCacheMode.CHAT,
- content,
- KeyGenerator.generateKey(content));
- }
-
- /**
- * Gets the identifier of the model used for the cached operation.
- *
- * @return The model identifier
- */
- public String model() {
- return model;
- }
-
- /**
- * Gets the seed value used for randomization in the cached operation.
- *
- * @return The seed value
- */
- public int seed() {
- return seed;
- }
-
- /**
- * Gets the temperature setting used in the cached operation.
- *
- * @return The temperature value
- */
- public double temperature() {
- return temperature;
- }
-
- /**
- * Gets the content that was processed in the cached operation.
- *
- * @return The content
- */
- public String content() {
- return content;
- }
-
- @Override
- @JsonIgnore
- public String localKey() {
- return localKey;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) return true;
- if (obj == null || obj.getClass() != this.getClass()) return false;
- var that = (ClassifierCacheKey) obj;
- return Objects.equals(this.model, that.model)
- && this.seed == that.seed
- && Double.doubleToLongBits(this.temperature) == Double.doubleToLongBits(that.temperature)
- && Objects.equals(this.mode, that.mode)
- && Objects.equals(this.content, that.content)
- && Objects.equals(this.localKey, that.localKey);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(model, seed, temperature, mode, content, localKey);
- }
-
- @Override
- public String toString() {
- return "ClassifierCacheKey[" + "model=" + model + ", " + "seed=" + seed + ", " + "temperature=" + temperature
- + ", " + "mode=" + mode + ", " + "content=" + content + ", " + "localKey=" + localKey + ']';
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheParameter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheParameter.java
deleted file mode 100644
index 63fb2f28..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheParameter.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache.classifier;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheParameter;
-
-/**
- * Cache parameters for classifier operations.
- * This record encapsulates the configuration parameters that define a unique classifier cache,
- * including the model name, random seed, and temperature setting.
- *
- * @param modelName The name of the language model used for classification
- * @param seed The random seed for reproducible results
- * @param temperature The temperature parameter for controlling randomness in model outputs
- */
-public record ClassifierCacheParameter(String modelName, int seed, double temperature)
- implements CacheParameter {
- @Override
- public String parameters() {
- // For backward compatibility, omit temperature if it is 0.0
- if (temperature == 0.0) {
- return String.join("_", modelName, String.valueOf(seed));
- } else {
- return String.join("_", modelName, String.valueOf(seed), String.valueOf(temperature));
- }
- }
-
- @Override
- public ClassifierCacheKey createCacheKey(String content) {
- return ClassifierCacheKey.of(this, content);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheKey.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheKey.java
deleted file mode 100644
index c7fae674..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheKey.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache.embedding;
-
-import java.util.Objects;
-
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheKey;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.LargeLanguageModelCacheMode;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
-
-/**
- * Represents a key for embedding caching operations in the LiSSA framework.
- * This class is used to uniquely identify cached values based on various parameters
- * such as the model used, seed value, operation mode, and content.
- *
- * The key can be serialized to JSON for storage and retrieval from the cache.
- */
-@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public final class EmbeddingCacheKey implements CacheKey {
- private final String model;
- private final int seed;
- private final double temperature;
- private final LargeLanguageModelCacheMode mode;
- private final String content;
-
- @JsonIgnore
- private final String localKey;
-
- /**
- * Creates a new embedding cache key with the specified parameters.
- *
- * @param model The identifier of the model used for the cached operation
- * @param seed The seed value used for randomization in the cached operation (-1 for backward compatibility)
- * @param temperature The temperature setting used in the cached operation (-1 for backward compatibility)
- * @param mode The mode of operation that was cached (embedding generation for backward compatibility)
- * @param content The content that was processed in the cached operation
- * @param localKey A local key for additional identification, not included in JSON serialization
- */
- private EmbeddingCacheKey(
- String model,
- int seed,
- double temperature,
- LargeLanguageModelCacheMode mode,
- String content,
- String localKey) {
- this.model = model;
- this.seed = seed;
- this.temperature = temperature;
- this.mode = mode;
- this.content = content;
- this.localKey = localKey;
- }
-
- /**
- * Creates an embedding cache key from the given cache parameter and content.
- * This is the preferred way to create cache keys.
- *
- * @param cacheParameter The cache parameter containing model configuration
- * @param content The content to be cached
- * @return A new embedding cache key
- */
- static EmbeddingCacheKey of(EmbeddingCacheParameter cacheParameter, String content) {
- return new EmbeddingCacheKey(
- cacheParameter.modelName(),
- -1,
- -1,
- LargeLanguageModelCacheMode.EMBEDDING,
- content,
- KeyGenerator.generateKey(content));
- }
-
- /**
- * Creates an embedding cache key with a custom local key.
- * Only use this method if you want to use a custom local key. You mostly do not want to do this.
- * Only for special handling of embeddings. You should always prefer the {@link #of(EmbeddingCacheParameter, String)} method.
- *
- * @param model The identifier of the model
- * @param content The content to be cached
- * @param localKey The custom local key
- * @return A new embedding cache key with the specified local key
- * @deprecated Please use {@link #of(EmbeddingCacheParameter, String)} instead
- */
- @Deprecated(forRemoval = false)
- public static EmbeddingCacheKey ofRaw(String model, String content, String localKey) {
- return new EmbeddingCacheKey(model, -1, -1, LargeLanguageModelCacheMode.EMBEDDING, content, localKey);
- }
-
- /**
- * Gets the identifier of the model used for the cached operation.
- *
- * @return The model identifier
- */
- public String model() {
- return model;
- }
-
- /**
- * Gets the content that was processed in the cached operation.
- *
- * @return The content
- */
- public String content() {
- return content;
- }
-
- @Override
- @JsonIgnore
- public String localKey() {
- return localKey;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) return true;
- if (obj == null || obj.getClass() != this.getClass()) return false;
- var that = (EmbeddingCacheKey) obj;
- return Objects.equals(this.model, that.model)
- && this.seed == that.seed
- && Double.doubleToLongBits(this.temperature) == Double.doubleToLongBits(that.temperature)
- && Objects.equals(this.mode, that.mode)
- && Objects.equals(this.content, that.content)
- && Objects.equals(this.localKey, that.localKey);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(model, seed, temperature, mode, content, localKey);
- }
-
- @Override
- public String toString() {
- return "EmbeddingCacheKey[" + "model=" + model + ", " + "seed=" + seed + ", " + "temperature=" + temperature
- + ", " + "mode=" + mode + ", " + "content=" + content + ", " + "localKey=" + localKey + ']';
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheParameter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheParameter.java
deleted file mode 100644
index 9afa7258..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheParameter.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache.embedding;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheParameter;
-
-/**
- * Cache parameters for embedding operations.
- * This record encapsulates the configuration parameters that define a unique embedding cache.
- * For embeddings, only the model name is required as embeddings are deterministic.
- *
- * @param modelName The name of the embedding model used for generating embeddings
- */
-public record EmbeddingCacheParameter(String modelName) implements CacheParameter {
- @Override
- public String parameters() {
- return modelName;
- }
-
- @Override
- public EmbeddingCacheKey createCacheKey(String content) {
- return EmbeddingCacheKey.of(this, content);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelPlatform.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelPlatform.java
deleted file mode 100644
index 4fa0a18f..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelPlatform.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.classifier;
-
-import static edu.kit.kastel.sdq.lissa.ratlr.configuration.Configuration.CONFIG_NAME_SEPARATOR;
-
-import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
-
-/**
- * Enum representing supported chat language model platforms.
- * Each platform specifies the number of threads to use for parallel execution.
- *
- *
- * - OPENAI: OpenAI platform (100 threads)
- * - OLLAMA: Ollama platform (1 thread)
- * - BLABLADOR: Blablador platform (100 threads)
- * - DEEPSEEK: DeepSeek platform (1 thread)
- *
- *
- * @see ChatLanguageModelProvider
- */
-public enum ChatLanguageModelPlatform {
- /**
- * OpenAI platform (100 threads).
- */
- OPENAI(100, "gpt-4o-mini"),
- /**
- * Ollama platform (1 thread).
- */
- OLLAMA(1, "llama3:8b"),
- /**
- * Blablador platform (100 threads).
- */
- BLABLADOR(100, "2 - Llama 3.3 70B instruct"),
- /**
- * DeepSeek platform (1 thread).
- */
- DEEPSEEK(1, "deepseek-chat"),
- /**
- * Open WebUI platform (1 thread, default model: "llama3:8b").
- */
- OPENWEBUI(10, "llama3:8b");
-
- private final int threads;
- private final String defaultModel;
-
- ChatLanguageModelPlatform(int threads, String defaultModel) {
- this.threads = threads;
- this.defaultModel = defaultModel;
- }
-
- /**
- * Returns the number of threads for this platform.
- *
- * @return the thread count
- */
- public int getThreads() {
- return threads;
- }
-
- /**
- * Returns the default model name for this platform.
- *
- * @return the default model name
- */
- public String getDefaultModel() {
- return defaultModel;
- }
-
- /**
- * Returns the enum value for the given platform name (case-insensitive).
- *
- * @param moduleConfiguration the configuration containing the platform name
- * @return the corresponding enum value
- * @throws IllegalArgumentException if the name does not match any platform
- */
- public static ChatLanguageModelPlatform fromModuleConfiguration(ModuleConfiguration moduleConfiguration) {
- String[] modeXplatform = moduleConfiguration.name().split(CONFIG_NAME_SEPARATOR, 2);
- if (modeXplatform.length < 2) {
- throw new IllegalArgumentException("Invalid configuration name: '%s'. Expected format: %s"
- .formatted(moduleConfiguration.name(), CONFIG_NAME_SEPARATOR));
- }
-
- String name = modeXplatform[1];
-
- for (ChatLanguageModelPlatform languageModelPlatform : values()) {
- if (languageModelPlatform.name().equalsIgnoreCase(name)) {
- return languageModelPlatform;
- }
- }
- throw new IllegalArgumentException("Unknown platform: " + name);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelProvider.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelProvider.java
index 33d41338..f5077963 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelProvider.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelProvider.java
@@ -1,315 +1,112 @@
/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr.classifier;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.Base64;
-import java.util.Map;
+import static edu.kit.kastel.sdq.lissa.ratlr.configuration.Configuration.CONFIG_NAME_SEPARATOR;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheParameter;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheParameter;
+import edu.kit.kastel.mcse.ardoco.llm.cache.chat.ChatCacheParameter;
+import edu.kit.kastel.mcse.ardoco.llm.chat.ChatModelPlatform;
+import edu.kit.kastel.mcse.ardoco.llm.chat.ChatModelProvider;
+import edu.kit.kastel.mcse.ardoco.llm.chat.LlmConfiguration;
import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.ollama.OllamaChatModel;
-import dev.langchain4j.model.openai.OpenAiChatModel;
/**
- * Provides chat language model instances for different platforms.
- * This class supports multiple language model platforms (OpenAI, Ollama, Blablador)
- * and handles their configuration, including authentication and model settings.
- *
- * Required environment variables for each platform:
- *
- * - OpenAI:
- *
- * - {@code OPENAI_ORGANIZATION_ID}: Your OpenAI organization ID
- * - {@code OPENAI_API_KEY}: Your OpenAI API key
- *
- *
- * - Ollama:
- *
- * - {@code OLLAMA_HOST}: The host URL for the Ollama server
- * - {@code OLLAMA_USER}: Username for Ollama authentication (optional)
- * - {@code OLLAMA_PASSWORD}: Password for Ollama authentication (optional)
- *
- *
- * - Blablador:
- *
- * - {@code BLABLADOR_API_KEY}: Your Blablador API key
- *
- *
- * - DeepSeek:
- *
- * - {@code DEEPSEEK_API_KEY}: Your DeepSeek API key
- *
- *
- * - Open WebUI:
- *
- * - {@code OPENWEBUI_URL}: The URL for the Open WebUI server
- * - {@code OPENWEBUI_API_KEY}: Your Open WebUI API key
- *
- *
- *
- *
- * @see ChatLanguageModelPlatform
+ * Adapts LiSSA's {@link ModuleConfiguration} (whose name has the form {@code _}) to the
+ * framework-neutral {@link ChatModelProvider} of the {@code llm-access} library. This keeps the existing
+ * classifier call sites unchanged while delegating the actual model creation and cache identification to
+ * the library.
*/
public class ChatLanguageModelProvider {
- /**
- * Default seed value for model.
- */
- public static final int DEFAULT_SEED = 133742243;
- /**
- * Default temperature setting for the model.
- */
- public static final double DEFAULT_TEMPERATURE = 0.0;
+ private final ChatModelProvider delegate;
/**
- * The platform to use for the language model.
- */
- private final ChatLanguageModelPlatform platform;
-
- /**
- * The name of the model to use.
- */
- private String modelName;
-
- /**
- * The seed value for model randomization.
- */
- private int seed;
-
- /**
- * Temperature setting for the model.
- */
- private double temperature;
-
- /**
- * Creates a new chat language model provider with the specified configuration.
- * The configuration name should be in the format "mode_platform" (e.g., "simple_openai").
+ * Creates a provider for the given module configuration.
*
- * @param configuration The module configuration containing model settings
+ * @param configuration The module configuration ({@code _} with optional model/seed/temperature args)
*/
public ChatLanguageModelProvider(ModuleConfiguration configuration) {
- this.platform = ChatLanguageModelPlatform.fromModuleConfiguration(configuration);
- this.initPlatformParameters(configuration);
+ this.delegate = new ChatModelProvider(toLlmConfiguration(configuration));
}
/**
- * Creates a chat model instance based on the configured platform.
+ * Creates a chat model instance for the configured platform.
*
- * @return A chat model instance for the configured platform
- * @throws IllegalArgumentException If the platform is not supported
+ * @return A chat model instance
*/
public ChatModel createChatModel() {
- return switch (platform) {
- case OPENAI -> createOpenAiChatModel(modelName, seed, temperature);
- case OLLAMA -> createOllamaChatModel(modelName, seed, temperature);
- case BLABLADOR -> createBlabladorChatModel(modelName, seed, temperature);
- case DEEPSEEK -> createDeepSeekChatModel(modelName, seed, temperature);
- case OPENWEBUI -> createOpenWebUIChatModel(modelName, seed, temperature);
- };
+ return delegate.createChatModel();
}
/**
- * Initializes the model platform settings from the configuration.
- * Sets the model name and seed value based on the platform.
- *
- * @param configuration The module configuration containing model settings
- * @throws IllegalArgumentException If the platform is not supported
- */
- private void initPlatformParameters(ModuleConfiguration configuration) {
- final String modelKey = "model";
- this.modelName = configuration.argumentAsString(modelKey, platform.getDefaultModel());
- this.seed = configuration.argumentAsInt("seed", DEFAULT_SEED);
- this.temperature = configuration.argumentAsDouble("temperature", DEFAULT_TEMPERATURE);
- }
-
- /**
- * Gets the name of the configured model.
+ * Gets the configured model name.
*
* @return The model name
*/
public String modelName() {
- return modelName;
+ return delegate.modelName();
}
/**
- * Gets the seed value used for model randomization.
+ * Gets the configured seed value.
*
* @return The seed value
*/
public int seed() {
- return seed;
+ return delegate.seed();
}
/**
- * Gets the temperature setting for the model.
+ * Gets the configured temperature.
*
* @return The temperature value
*/
public double temperature() {
- return temperature;
+ return delegate.temperature();
}
/**
- * Determines the number of threads to use based on the platform.
- * OpenAI and Blablador platforms use 100 threads, while others use 1.
+ * Returns the cache parameters that uniquely identify the model configuration.
*
- * @param configuration The module configuration
- * @return The number of threads to use
+ * @return The chat cache parameters
*/
- public static int threads(ModuleConfiguration configuration) {
- return ChatLanguageModelPlatform.fromModuleConfiguration(configuration).getThreads();
+ public ChatCacheParameter cacheParameters() {
+ return delegate.cacheParameters();
}
/**
- * Creates an Ollama chat model instance.
- * The model is configured with authentication if credentials are provided.
+ * Determines the number of threads to use for the platform of the given configuration.
*
- * @param model The name of the model to use
- * @param seed The seed value for randomization
- * @param temperature The temperature setting for the model
- * @return A configured Ollama chat model instance
- */
- private static ChatModel createOllamaChatModel(String model, int seed, double temperature) {
- String host = Environment.getenv("OLLAMA_HOST");
- String user = Environment.getenv("OLLAMA_USER");
- String password = Environment.getenv("OLLAMA_PASSWORD");
-
- if (host == null) {
- throw new IllegalStateException("OLLAMA_HOST environment variable not set");
- }
-
- return new LazyChatModel(() -> {
- var ollama = OllamaChatModel.builder()
- .baseUrl(host)
- .modelName(model)
- .timeout(Duration.ofMinutes(10))
- .temperature(temperature)
- .seed(seed);
- if (user != null && password != null && !user.isEmpty() && !password.isEmpty()) {
- ollama.customHeaders(Map.of(
- "Authorization",
- "Basic "
- + Base64.getEncoder()
- .encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8))));
- }
- return ollama.build();
- });
- }
-
- /**
- * Creates an OpenAI chat model instance.
- * Requires OpenAI organization ID and API key to be set in environment variables.
- *
- * @param model The name of the model to use
- * @param seed The seed value for randomization
- * @param temperature The temperature setting for the model
- * @return A configured OpenAI chat model instance
- * @throws IllegalStateException If required environment variables are not set
+ * @param configuration The module configuration
+ * @return The number of threads to use
*/
- private static ChatModel createOpenAiChatModel(String model, int seed, double temperature) {
- String openAiOrganizationId = Environment.getenv("OPENAI_ORGANIZATION_ID");
- String openAiApiKey = Environment.getenv("OPENAI_API_KEY");
- if (openAiOrganizationId == null || openAiApiKey == null) {
- throw new IllegalStateException("OPENAI_ORGANIZATION_ID or OPENAI_API_KEY environment variable not set");
- }
-
- return new LazyChatModel(() -> new OpenAiChatModel.OpenAiChatModelBuilder()
- .modelName(model)
- .organizationId(openAiOrganizationId)
- .apiKey(openAiApiKey)
- .temperature(temperature)
- .seed(seed)
- .build());
+ public static int threads(ModuleConfiguration configuration) {
+ return switch (platform(configuration)) {
+ case OPENAI, BLABLADOR, DEEPSEEK -> 100;
+ case OPENWEBUI -> 10;
+ case OLLAMA -> 1;
+ };
}
- /**
- * Creates a Blablador chat model instance.
- * Requires Blablador API key to be set in environment variables.
- *
- * @param model The name of the model to use
- * @param seed The seed value for randomization
- * @param temperature The temperature setting for the model
- * @return A configured Blablador chat model instance
- * @throws IllegalStateException If required environment variables are not set
- */
- private static ChatModel createBlabladorChatModel(String model, int seed, double temperature) {
- String blabladorApiKey = Environment.getenv("BLABLADOR_API_KEY");
- if (blabladorApiKey == null) {
- throw new IllegalStateException("BLABLADOR_API_KEY environment variable not set");
- }
- return new LazyChatModel(() -> new OpenAiChatModel.OpenAiChatModelBuilder()
- .baseUrl("https://api.helmholtz-blablador.fz-juelich.de/v1")
+ private static LlmConfiguration toLlmConfiguration(ModuleConfiguration configuration) {
+ ChatModelPlatform platform = platform(configuration);
+ String model = configuration.argumentAsString("model");
+ int seed = configuration.argumentAsInt("seed", LlmConfiguration.DEFAULT_SEED);
+ double temperature = configuration.argumentAsDouble("temperature", LlmConfiguration.DEFAULT_TEMPERATURE);
+ return LlmConfiguration.builder(platform)
.modelName(model)
- .apiKey(blabladorApiKey)
- .temperature(temperature)
.seed(seed)
- .build());
- }
-
- /**
- * Creates a DeepSeek chat model instance.
- * Requires DeepSeek API key to be set in environment variables.
- *
- * @param model The name of the model to use
- * @param seed The seed value for randomization
- * @param temperature The temperature setting for the model
- * @return A configured DeepSeek chat model instance
- * @throws IllegalStateException If required environment variables are not set
- */
- private static ChatModel createDeepSeekChatModel(String model, int seed, double temperature) {
- String deepseekApiKey = Environment.getenv("DEEPSEEK_API_KEY");
- if (deepseekApiKey == null) {
- throw new IllegalStateException("DEEPSEEK_API_KEY environment variable not set");
- }
- return new LazyChatModel(() -> new OpenAiChatModel.OpenAiChatModelBuilder()
- .baseUrl("https://api.deepseek.com/v1")
- .modelName(model)
- .apiKey(deepseekApiKey)
.temperature(temperature)
- .seed(seed)
- .build());
+ .build();
}
- /**
- * Creates an Open WebUI chat model instance.
- * Requires Open WebUI API key and url to be set in environment variables.
- *
- * @param model The name of the model to use
- * @param seed The seed value for randomization
- * @param temperature The temperature setting for the model
- * @return A configured Open WebUI chat model instance
- * @throws IllegalStateException If required environment variables are not set
- */
- private static ChatModel createOpenWebUIChatModel(String model, int seed, double temperature) {
- String openwebuiUrl = Environment.getenv("OPENWEBUI_URL");
- String openwebuiApiKey = Environment.getenv("OPENWEBUI_API_KEY");
- if (openwebuiUrl == null || openwebuiApiKey == null) {
- throw new IllegalStateException("OPENWEBUI_URL or OPENWEBUI_API_KEY environment variable not set");
+ private static ChatModelPlatform platform(ModuleConfiguration configuration) {
+ String[] modeXplatform = configuration.name().split(CONFIG_NAME_SEPARATOR, 2);
+ if (modeXplatform.length < 2) {
+ throw new IllegalArgumentException("Invalid configuration name: '%s'. Expected format: %s"
+ .formatted(configuration.name(), CONFIG_NAME_SEPARATOR));
}
-
- return new LazyChatModel(() -> new OpenAiChatModel.OpenAiChatModelBuilder()
- .baseUrl(openwebuiUrl)
- .modelName(model)
- .apiKey(openwebuiApiKey)
- .temperature(temperature)
- .seed(seed)
- .timeout(Duration.ofMinutes(10))
- .build());
- }
-
- /**
- * Returns the parameters used to create the cache key for this model.
- * This method is used to identify the cache uniquely.
- *
- * @return An array of strings representing the cache parameters
- * @see edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager#getCache(Object, CacheParameter)
- */
- public ClassifierCacheParameter cacheParameters() {
- return new ClassifierCacheParameter(modelName, seed, temperature);
+ return ChatModelPlatform.fromString(modeXplatform[1]);
}
}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/LazyChatModel.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/LazyChatModel.java
deleted file mode 100644
index 69d8abab..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/LazyChatModel.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.classifier;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.Supplier;
-
-import org.jspecify.annotations.Nullable;
-
-import dev.langchain4j.data.message.ChatMessage;
-import dev.langchain4j.model.ModelProvider;
-import dev.langchain4j.model.chat.Capability;
-import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.chat.ChatRequestOptions;
-import dev.langchain4j.model.chat.listener.ChatModelListener;
-import dev.langchain4j.model.chat.request.ChatRequest;
-import dev.langchain4j.model.chat.request.ChatRequestParameters;
-import dev.langchain4j.model.chat.response.ChatResponse;
-
-/**
- * A chat model that initializes its delegate lazily using a supplier.
- * This allows for deferred creation of the underlying chat model until it is actually needed.
- * The delegate is initialized in a thread-safe manner, ensuring that only one instance is created
- * even if multiple threads access the model concurrently.
- */
-public final class LazyChatModel implements ChatModel {
-
- private final Supplier delegateSupplier;
-
- private volatile @Nullable ChatModel delegate;
-
- /**
- * Creates a new LazyChatModel with the specified supplier for the delegate.
- *
- * @param delegateSupplier The supplier that provides the chat model delegate
- */
- public LazyChatModel(Supplier delegateSupplier) {
- this.delegateSupplier = Objects.requireNonNull(delegateSupplier);
- }
-
- private ChatModel delegate() {
- if (delegate != null) {
- return delegate;
- }
- synchronized (this) {
- if (delegate == null) {
- delegate = delegateSupplier.get();
- }
- return delegate;
- }
- }
-
- @Override
- public ChatResponse chat(ChatRequest chatRequest) {
- return delegate().chat(chatRequest);
- }
-
- @Override
- public ChatResponse chat(ChatRequest chatRequest, ChatRequestOptions options) {
- return delegate().chat(chatRequest, options);
- }
-
- @Override
- public ChatResponse doChat(ChatRequest chatRequest) {
- return delegate().doChat(chatRequest);
- }
-
- @Override
- public ChatRequestParameters defaultRequestParameters() {
- return delegate().defaultRequestParameters();
- }
-
- @Override
- public List listeners() {
- return delegate().listeners();
- }
-
- @Override
- public ModelProvider provider() {
- return delegate().provider();
- }
-
- @Override
- public String chat(String userMessage) {
- return delegate().chat(userMessage);
- }
-
- @Override
- public ChatResponse chat(ChatMessage... messages) {
- return delegate().chat(messages);
- }
-
- @Override
- public ChatResponse chat(List messages) {
- return delegate().chat(messages);
- }
-
- @Override
- public Set supportedCapabilities() {
- return delegate().supportedCapabilities();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ReasoningClassifier.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ReasoningClassifier.java
index 3d7c5f51..b639397b 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ReasoningClassifier.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ReasoningClassifier.java
@@ -9,9 +9,9 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.Cache;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.cache.Cache;
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager;
+import edu.kit.kastel.mcse.ardoco.llm.cache.chat.ChatCacheKey;
import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
@@ -36,7 +36,7 @@ public class ReasoningClassifier extends Classifier {
*/
private static final String CLASSIFICATION_PROMPT_KEY = "prompt";
- private final Cache cache;
+ private final Cache cache;
/**
* Provider for the language model used in classification.
@@ -96,7 +96,7 @@ public ReasoningClassifier(ModuleConfiguration configuration, ContextStore conte
*/
private ReasoningClassifier(
int threads,
- Cache cache,
+ Cache cache,
ChatLanguageModelProvider provider,
String prompt,
boolean useOriginalArtifacts,
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/SimpleClassifier.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/SimpleClassifier.java
index 63bb61e9..d5a8976d 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/SimpleClassifier.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/SimpleClassifier.java
@@ -3,9 +3,9 @@
import java.util.Optional;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.Cache;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.cache.Cache;
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager;
+import edu.kit.kastel.mcse.ardoco.llm.cache.chat.ChatCacheKey;
import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
@@ -47,7 +47,7 @@ public class SimpleClassifier extends Classifier {
/**
* The cache used for storing classification results.
*/
- private final Cache cache;
+ private final Cache cache;
/**
* Provider for the language model used in classification.
@@ -89,7 +89,7 @@ public SimpleClassifier(ModuleConfiguration configuration, ContextStore contextS
*/
private SimpleClassifier(
int threads,
- Cache cache,
+ Cache cache,
ChatLanguageModelProvider provider,
String template,
ContextStore contextStore) {
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/configuration/Configuration.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/configuration/Configuration.java
index 89bc8a2d..6f33d54b 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/configuration/Configuration.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/configuration/Configuration.java
@@ -3,7 +3,7 @@
import java.util.Objects;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
+import edu.kit.kastel.mcse.ardoco.llm.util.KeyGenerator;
/**
* Base interface for all configuration types in the LiSSA-RATLR framework.
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStore.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStore.java
index 89dfe3c2..d3375283 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStore.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStore.java
@@ -1,4 +1,4 @@
-/* Licensed under MIT 2025. */
+/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr.elementstore;
import java.util.ArrayList;
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/SourceElementStore.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/SourceElementStore.java
index f43a901a..48a2d76f 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/SourceElementStore.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/SourceElementStore.java
@@ -1,4 +1,4 @@
-/* Licensed under MIT 2025. */
+/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr.elementstore;
import java.util.List;
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/TargetElementStore.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/TargetElementStore.java
index 2ce911e5..ada8030e 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/TargetElementStore.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/TargetElementStore.java
@@ -1,4 +1,4 @@
-/* Licensed under MIT 2025. */
+/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr.elementstore;
import java.util.List;
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/CachedEmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/CachedEmbeddingCreator.java
deleted file mode 100644
index d2423d51..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/CachedEmbeddingCreator.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
-
-import java.util.*;
-import java.util.concurrent.*;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.knuddels.jtokkit.Encodings;
-import com.knuddels.jtokkit.api.Encoding;
-import com.knuddels.jtokkit.api.EncodingRegistry;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.*;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.embedding.EmbeddingCacheKey;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.embedding.EmbeddingCacheParameter;
-import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
-import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Futures;
-
-import dev.langchain4j.model.embedding.EmbeddingModel;
-
-/**
- * Abstract base class for embedding creators that implement caching functionality.
- * This class provides a framework for creating and caching embeddings with support for:
- *
- * - Multi-threaded embedding generation
- * - Automatic caching of embeddings to improve performance
- * - Handling of long texts through token length management
- * - Fallback mechanisms for failed embedding generation
- *
- *
- * The class uses a cache to store previously generated embeddings and implements
- * a sophisticated mechanism to handle texts that exceed the maximum token length
- * of the underlying embedding model.
- */
-abstract class CachedEmbeddingCreator extends EmbeddingCreator {
- // TODO Handle Token Length better .. 8192 is the length for ada
- private static final int MAX_TOKEN_LENGTH = 8000;
-
- private static final Logger STATIC_LOGGER = LoggerFactory.getLogger(CachedEmbeddingCreator.class);
- protected final Logger logger = LoggerFactory.getLogger(this.getClass());
- private final Cache cache;
- private final EmbeddingModel embeddingModel;
- private final String rawNameOfModel;
- private final int threads;
- private final EmbeddingCacheParameter embeddingCacheParameter;
-
- /**
- * Creates a new cached embedding creator with the specified model and thread count.
- *
- * @param contextStore The shared context store for pipeline components
- * @param model The name of the embedding model to use
- * @param threads The number of threads to use for parallel embedding generation
- * @param params Additional parameters for the embedding model
- */
- protected CachedEmbeddingCreator(ContextStore contextStore, String model, int threads, String... params) {
- super(contextStore);
- this.embeddingCacheParameter = new EmbeddingCacheParameter(model);
- this.cache = CacheManager.getDefaultInstance().getCache(this, embeddingCacheParameter);
- this.embeddingModel = Objects.requireNonNull(createEmbeddingModel(model, params));
- this.rawNameOfModel = model;
- this.threads = Math.max(1, threads);
- }
-
- /**
- * Creates an instance of the embedding model with the specified parameters.
- * This method must be implemented by concrete subclasses to provide the actual
- * model creation logic.
- *
- * @param model The name of the model to create
- * @param params Additional parameters for model creation
- * @return A new instance of the embedding model
- */
- protected abstract EmbeddingModel createEmbeddingModel(String model, String... params);
-
- /**
- * Calculates embeddings for a list of elements, using either sequential or parallel processing
- * based on the configured thread count.
- *
- * @param elements The list of elements to create embeddings for
- * @return A list of vector embeddings, in the same order as the input elements
- */
- @Override
- public final List calculateEmbeddings(List elements) {
- if (threads == 1) return calculateEmbeddingsSequential(elements);
-
- int threadCount = Math.min(threads, elements.size());
- int numberOfElementsPerThread = elements.size() / threadCount;
- ExecutorService executor = Executors.newFixedThreadPool(threadCount);
- List>> futureResults = new ArrayList<>();
-
- for (int i = 0; i < threadCount; i++) {
- int start = i * numberOfElementsPerThread;
- int end = i == threadCount - 1 ? elements.size() : (i + 1) * numberOfElementsPerThread;
- List subList = elements.subList(start, end);
- futureResults.add(executor.submit(() -> {
- var embeddingModelInstance = createEmbeddingModel(this.rawNameOfModel);
- return calculateEmbeddingsSequential(embeddingModelInstance, subList);
- }));
- }
- logger.info("Waiting for classification to finish. Elements in queue: {}", futureResults.size());
-
- try {
- executor.shutdown();
- boolean success = executor.awaitTermination(1, TimeUnit.DAYS);
- if (!success) {
- logger.error("Embedding did not finish in time.");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- executor.close();
-
- return futureResults.stream()
- .map(f -> Futures.getLogged(f, logger))
- .flatMap(Collection::stream)
- .toList();
- }
-
- /**
- * Calculates embeddings sequentially using the default embedding model.
- *
- * @param elements The list of elements to create embeddings for
- * @return A list of vector embeddings
- */
- private List calculateEmbeddingsSequential(List elements) {
- return this.calculateEmbeddingsSequential(this.embeddingModel, elements);
- }
-
- /**
- * Calculates embeddings sequentially using the specified embedding model.
- *
- * @param embeddingModel The model to use for embedding generation
- * @param elements The list of elements to create embeddings for
- * @return A list of vector embeddings
- */
- private List calculateEmbeddingsSequential(EmbeddingModel embeddingModel, List elements) {
- List embeddings = new ArrayList<>();
- for (Element element : elements) {
- embeddings.add(calculateFinalEmbedding(embeddingModel, cache, embeddingCacheParameter, element));
- }
- return embeddings;
- }
-
- /**
- * Calculates the final embedding for an element, using the cache if available.
- * This method implements a sophisticated caching and error handling strategy:
- *
- * - First, generates a unique cache key based on the element's content
- * - Checks if a cached embedding exists for this key
- * - If cached, returns the existing embedding immediately
- * - If not cached:
- *
- * - Attempts to generate a new embedding using the provided model
- * - If successful, caches the result and returns it
- * - If generation fails (e.g., due to token length), attempts to fix the issue
- * using {@link #tryToFixWithLength}
- *
- *
- *
- *
- * The method uses a composite cache key that includes:
- *
- * - The model name
- * - The operation mode (EMBEDDING)
- * - The original content
- * - A generated key based on the content
- *
- *
- * @param embeddingModel The model to use for embedding generation
- * @param cache The cache to use for storing and retrieving embeddings
- * @param embeddingCacheParameter The EmbeddingCacheParameter of the model being used
- * @param element The element to create an embedding for
- * @return The vector embedding of the element, either from cache or newly generated
- */
- private static float[] calculateFinalEmbedding(
- EmbeddingModel embeddingModel,
- Cache cache,
- EmbeddingCacheParameter embeddingCacheParameter,
- Element element) {
-
- String elementContent = element.getContent();
- float[] cachedEmbedding = cache.get(elementContent, float[].class);
- if (cachedEmbedding != null) {
- return cachedEmbedding;
- } else {
- STATIC_LOGGER.info("Calculating embedding for: {}", element.getIdentifier());
- try {
- float[] embedding =
- embeddingModel.embed(elementContent).content().vector();
- cache.put(elementContent, embedding);
- return embedding;
- } catch (Exception e) {
- STATIC_LOGGER.error(
- "Error while calculating embedding for .. try to fix ..: {}", element.getIdentifier());
- // Probably the length was too long .. check that
- return tryToFixWithLength(embeddingModel, cache, embeddingCacheParameter.modelName(), elementContent);
- }
- }
- }
-
- /**
- * Attempts to fix embedding generation for content that exceeds the maximum token length.
- * This method uses binary search to find the maximum content length that fits within
- * the token limit and generates an embedding for that truncated content.
- *
- * @param embeddingModel The model to use for embedding generation
- * @param cache The cache to use for storing and retrieving embeddings
- * @param rawNameOfModel The name of the model being used
- * @param content The content that exceeded the token limit
- * @return The vector embedding of the truncated content
- * @throws IllegalArgumentException If the token length was not the cause of the failure
- */
- private static float[] tryToFixWithLength(
- EmbeddingModel embeddingModel, Cache cache, String rawNameOfModel, String content) {
- EmbeddingCacheKey originalKey = cache.getCacheParameter().createCacheKey(content);
- String newKey = originalKey.localKey() + "_fixed_" + MAX_TOKEN_LENGTH;
-
- // We need the old keys for backwards compatibility
- @SuppressWarnings("deprecation")
- EmbeddingCacheKey newCacheKey =
- EmbeddingCacheKey.ofRaw(rawNameOfModel, "(FIXED::%d): %s".formatted(MAX_TOKEN_LENGTH, content), newKey);
-
- @SuppressWarnings("deprecation")
- float[] cachedEmbedding = cache.getViaInternalKey(newCacheKey, float[].class);
- if (cachedEmbedding != null) {
- if (STATIC_LOGGER.isInfoEnabled()) {
- STATIC_LOGGER.info("using fixed embedding for: {}", originalKey.localKey());
- }
- return cachedEmbedding;
- }
- EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
- Encoding encoding = registry.getEncodingForModel(rawNameOfModel)
- .orElseThrow(() -> new IllegalArgumentException(
- "Unknown Embedding Model. Don't know how to handle previous exception"));
- int tokens = encoding.countTokens(content);
- if (tokens < MAX_TOKEN_LENGTH)
- throw new IllegalArgumentException(
- "Token length was not too long. Don't know how to handle previous exception");
-
- // Binary search for max length of string
- int left = 0;
- int right = content.length();
- while (left < right) {
- int mid = left + (right - left) / 2;
- String subContent = content.substring(0, mid);
- int subTokens = encoding.countTokens(subContent);
- if (subTokens >= MAX_TOKEN_LENGTH) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- String fixedContent = content.substring(0, left);
- float[] embedding = embeddingModel.embed(fixedContent).content().vector();
- if (STATIC_LOGGER.isInfoEnabled()) {
- STATIC_LOGGER.info("using fixed embedding for: {}", originalKey.localKey());
- }
- cache.putViaInternalKey(newCacheKey, embedding);
- return embedding;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java
index 5f88f170..69c92344 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java
@@ -1,116 +1,89 @@
-/* Licensed under MIT 2025. */
+/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
import java.util.List;
import java.util.Objects;
+import edu.kit.kastel.mcse.ardoco.llm.embedding.EmbeddingConfiguration;
+import edu.kit.kastel.mcse.ardoco.llm.embedding.EmbeddingPlatform;
import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
/**
- * Abstract base class for creating vector embeddings of elements in the LiSSA framework.
- * This class provides the interface for different embedding creation strategies,
- * which convert text elements into vector representations for similarity matching.
- *
- * All embedding creators have access to a shared {@link edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore} via the protected {@code contextStore} field,
- * which is initialized in the constructor and available to all subclasses.
- * Subclasses should not duplicate context handling.
- *
- * The framework supports multiple embedding creation backends:
- *
- * - Ollama: Local embedding generation using Ollama models
- *
- * - Requires OLLAMA_EMBEDDING_HOST environment variable
- * - Optional authentication via OLLAMA_EMBEDDING_USER and OLLAMA_EMBEDDING_PASSWORD
- * - Default model: nomic-embed-text:v1.5
- *
- *
- * - OpenAI: Cloud-based embedding generation using OpenAI's API
- *
- * - Requires OPENAI_ORGANIZATION_ID and OPENAI_API_KEY environment variables
- * - Default model: text-embedding-ada-002
- * - Supports high-throughput with 40 parallel threads
- *
- *
- * - ONNX: Local embedding generation using ONNX models
- *
- * - Requires local model and tokenizer files
- * - Uses mean pooling for embedding generation
- * - Configuration via path_to_model and path_to_tokenizer parameters
- *
- *
- * - Mock: Testing implementation
- *
- * - Returns zero vectors for all elements
- * - Useful for testing without actual embedding generation
- *
- *
- *
- *
- * All implementations (except Mock) support caching of embeddings through the
- * {@link CachedEmbeddingCreator} base class, which provides:
- *
- * - Automatic caching of generated embeddings
- * - Handling of long texts through token length management
- * - Multi-threaded embedding generation where supported
- * - Fallback mechanisms for failed embedding generation
- *
+ * Adapts LiSSA's {@link Element}-based embedding usage and {@link ModuleConfiguration} to the
+ * framework-neutral {@link edu.kit.kastel.mcse.ardoco.llm.embedding.EmbeddingCreator} of the
+ * {@code llm-access} library. Embeddings are computed on the {@linkplain Element#getContent() element
+ * content}; caching and token-length handling are provided by the library.
*/
-public abstract class EmbeddingCreator {
- /**
- * The shared context store for pipeline components.
- * Available to all subclasses for accessing shared context.
- */
- protected final ContextStore contextStore;
+public class EmbeddingCreator {
- /**
- * Creates a new embedding creator with the specified context store.
- *
- * @param contextStore The shared context store for pipeline components
- */
- protected EmbeddingCreator(ContextStore contextStore) {
- this.contextStore = Objects.requireNonNull(contextStore);
+ private final edu.kit.kastel.mcse.ardoco.llm.embedding.EmbeddingCreator delegate;
+
+ private EmbeddingCreator(edu.kit.kastel.mcse.ardoco.llm.embedding.EmbeddingCreator delegate) {
+ this.delegate = Objects.requireNonNull(delegate);
}
/**
* Calculates the embedding for a single element.
- * This is a convenience method that delegates to {@link #calculateEmbeddings(List)}.
*
- * @param element The element to create an embedding for
- * @return The vector embedding of the element
+ * @param element The element to embed
+ * @return The vector embedding of the element's content
*/
public float[] calculateEmbedding(Element element) {
- return calculateEmbeddings(List.of(element)).getFirst();
+ return delegate.calculateEmbedding(element.getContent());
}
/**
* Calculates embeddings for a list of elements.
- * This method must be implemented by concrete embedding creators to provide
- * the actual embedding generation logic.
*
- * @param elements The list of elements to create embeddings for
+ * @param elements The elements to embed
* @return A list of vector embeddings, in the same order as the input elements
*/
- public abstract List calculateEmbeddings(List elements);
+ public List calculateEmbeddings(List elements) {
+ return delegate.calculateEmbeddings(
+ elements.stream().map(Element::getContent).toList());
+ }
/**
- * Creates an appropriate embedding creator based on the provided configuration.
+ * Creates an embedding creator based on the provided configuration.
* The type of creator is determined by the configuration's name field.
*
- * @param configuration The configuration specifying which embedding creator to use
- * @param contextStore The shared context store for pipeline components
- * @return An instance of the appropriate embedding creator
- * @throws IllegalStateException If the configuration specifies an unknown creator type
+ * @param configuration The configuration specifying the embedding creator
+ * @param contextStore The shared context store for pipeline components (kept for API compatibility)
+ * @return An embedding creator adapter
*/
public static EmbeddingCreator createEmbeddingCreator(
ModuleConfiguration configuration, ContextStore contextStore) {
+ Objects.requireNonNull(contextStore);
+ EmbeddingConfiguration embeddingConfiguration = toEmbeddingConfiguration(configuration);
+ return new EmbeddingCreator(
+ edu.kit.kastel.mcse.ardoco.llm.embedding.EmbeddingCreator.create(embeddingConfiguration));
+ }
+
+ private static EmbeddingConfiguration toEmbeddingConfiguration(ModuleConfiguration configuration) {
return switch (configuration.name()) {
- case "ollama" -> new OllamaEmbeddingCreator(configuration, contextStore);
- case "openai" -> new OpenAiEmbeddingCreator(configuration, contextStore);
- case "onnx" -> new OnnxEmbeddingCreator(configuration, contextStore);
- case "openwebui" -> new OpenWebUiEmbeddingCreator(configuration, contextStore);
- case "mock" -> new MockEmbeddingCreator(contextStore);
+ case "ollama" ->
+ EmbeddingConfiguration.builder(EmbeddingPlatform.OLLAMA)
+ .modelName(configuration.argumentAsString("model", "nomic-embed-text:v1.5"))
+ .build();
+ case "openai" ->
+ EmbeddingConfiguration.builder(EmbeddingPlatform.OPENAI)
+ .modelName(configuration.argumentAsString("model", "text-embedding-ada-002"))
+ .build();
+ case "onnx" ->
+ EmbeddingConfiguration.onnx(
+ configuration.argumentAsString("model"),
+ configuration.argumentAsString("path_to_model"),
+ configuration.argumentAsString("path_to_tokenizer"));
+ case "openwebui" ->
+ EmbeddingConfiguration.builder(EmbeddingPlatform.OPENWEBUI)
+ .modelName(configuration.argumentAsString("model", "nomic-embed-text:v1.5"))
+ .build();
+ // The mock creator ignores the model, so do not read a "model" argument here: that way a stray
+ // "model" on a mock configuration is reported as an unread (misconfigured) parameter.
+ case "mock" ->
+ EmbeddingConfiguration.builder(EmbeddingPlatform.MOCK).build();
default -> throw new IllegalStateException("Unexpected value: " + configuration.name());
};
}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/MockEmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/MockEmbeddingCreator.java
deleted file mode 100644
index 7393e4bf..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/MockEmbeddingCreator.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
-
-import java.util.List;
-
-import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
-import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
-
-/**
- * A mock implementation of the embedding creator that returns zero vectors for all elements.
- * This class serves two primary purposes:
- *
- * - Testing: Provides a simple way to test the embedding pipeline without
- * incurring the computational cost of real embedding generation
- * - Ensemble Mode: Can be used in the Ensemble Mode of LLMs (specified as "classifiers"
- * in the configuration) for multi-layered classification, where actual embeddings
- * are not required for the classification process
- *
- *
- * The implementation assigns a zero vector of length 1 to each element, making it
- * suitable for scenarios where the actual embedding values are not relevant to the
- * classification process.
- */
-public class MockEmbeddingCreator extends EmbeddingCreator {
- public MockEmbeddingCreator(ContextStore contextStore) {
- super(contextStore);
- }
-
- /**
- * Calculates mock embeddings for a list of elements.
- * Each element is assigned a zero vector of length 1.
- *
- * @param elements The list of elements to create mock embeddings for
- * @return A list of zero vectors, one for each input element
- */
- @Override
- public List calculateEmbeddings(List elements) {
- return elements.stream().map(it -> new float[] {0}).toList();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OllamaEmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OllamaEmbeddingCreator.java
deleted file mode 100644
index 595e731b..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OllamaEmbeddingCreator.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
-
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.Base64;
-import java.util.Map;
-
-import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
-import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-import dev.langchain4j.model.embedding.EmbeddingModel;
-import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
-
-/**
- * An embedding creator that uses Ollama for generating embeddings.
- * This class provides integration with Ollama's embedding models, supporting
- * both authenticated and unauthenticated access to the Ollama server.
- *
- * Required environment variables:
- *
- * - {@code OLLAMA_EMBEDDING_HOST}: The host URL of the Ollama server
- * - {@code OLLAMA_EMBEDDING_USER}: (Optional) Username for authentication
- * - {@code OLLAMA_EMBEDDING_PASSWORD}: (Optional) Password for authentication
- *
- *
- * The default model used is "nomic-embed-text:v1.5", but this can be overridden
- * through the configuration.
- */
-public class OllamaEmbeddingCreator extends CachedEmbeddingCreator {
-
- /**
- * Creates a new Ollama embedding creator with the specified configuration.
- * The configuration can specify a custom model name, otherwise the default
- * "nomic-embed-text:v1.5" is used.
- *
- * @param configuration The configuration containing model settings
- * @param contextStore The shared context store for pipeline components
- */
- public OllamaEmbeddingCreator(ModuleConfiguration configuration, ContextStore contextStore) {
- super(contextStore, configuration.argumentAsString("model", "nomic-embed-text:v1.5"), 1);
- }
-
- /**
- * Creates an Ollama embedding model instance with the specified parameters.
- * The method configures the model with authentication if credentials are provided
- * in the environment variables.
- *
- * @param model The name of the Ollama model to use
- * @param params Additional parameters (not used in this implementation)
- * @return A configured Ollama embedding model instance
- */
- @Override
- protected EmbeddingModel createEmbeddingModel(String model, String... params) {
- String host = Environment.getenvNonNull("OLLAMA_EMBEDDING_HOST");
- String user = Environment.getenv("OLLAMA_EMBEDDING_USER");
- String password = Environment.getenv("OLLAMA_EMBEDDING_PASSWORD");
-
- var ollamaEmbedding = new OllamaEmbeddingModel.OllamaEmbeddingModelBuilder()
- .baseUrl(host)
- .modelName(model)
- .timeout(Duration.ofMinutes(5));
- if (user != null && password != null && !user.isEmpty() && !password.isEmpty()) {
- ollamaEmbedding.customHeaders(Map.of(
- "Authorization",
- "Basic "
- + Base64.getEncoder()
- .encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8))));
- }
- return ollamaEmbedding.build();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OnnxEmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OnnxEmbeddingCreator.java
deleted file mode 100644
index bca7fe7a..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OnnxEmbeddingCreator.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
-
-import java.io.File;
-
-import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
-import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
-
-import dev.langchain4j.model.embedding.EmbeddingModel;
-import dev.langchain4j.model.embedding.onnx.OnnxEmbeddingModel;
-import dev.langchain4j.model.embedding.onnx.PoolingMode;
-
-/**
- * An embedding creator that uses ONNX models for generating embeddings.
- * This class provides integration with ONNX-based embedding models, allowing
- * for local embedding generation without requiring external services.
- *
- * The creator requires both a model file and a tokenizer file to be present
- * on the local filesystem. These files are specified either through the
- * constructor parameters or through the module configuration.
- *
- * The embedding model uses mean pooling by default for generating the final
- * embeddings from the token-level representations.
- */
-public class OnnxEmbeddingCreator extends CachedEmbeddingCreator {
- /**
- * Creates a new ONNX embedding creator with the specified model and file paths.
- *
- * @param model The name of the model
- * @param pathToModel The path to the ONNX model file
- * @param pathToTokenizer The path to the tokenizer file
- */
- public OnnxEmbeddingCreator(String model, String pathToModel, String pathToTokenizer, ContextStore contextStore) {
- super(contextStore, model, 1, pathToModel, pathToTokenizer);
- }
-
- /**
- * Creates a new ONNX embedding creator from a module configuration.
- * The configuration must specify:
- *
- * - {@code model}: The name of the model
- * - {@code path_to_model}: The path to the ONNX model file
- * - {@code path_to_tokenizer}: The path to the tokenizer file
- *
- *
- * @param configuration The configuration containing model and file paths
- * @param contextStore The shared context store for pipeline components
- */
- public OnnxEmbeddingCreator(ModuleConfiguration configuration, ContextStore contextStore) {
- this(
- configuration.argumentAsString("model"),
- configuration.argumentAsString("path_to_model"),
- configuration.argumentAsString("path_to_tokenizer"),
- contextStore);
- }
-
- /**
- * Creates an ONNX embedding model instance with the specified parameters.
- * The method verifies the existence of both the model and tokenizer files
- * before creating the model instance.
- *
- * @param model The name of the model
- * @param params Additional parameters containing the model and tokenizer file paths
- * @return A configured ONNX embedding model instance
- * @throws IllegalStateException If either the model or tokenizer file does not exist
- */
- @Override
- protected EmbeddingModel createEmbeddingModel(String model, String... params) {
- String modelPath = params[0];
- String tokenizerPath = params[1];
-
- File modelFile = new File(modelPath);
- File tokenizerFile = new File(tokenizerPath);
- if (!modelFile.exists() || !tokenizerFile.exists()) {
- throw new IllegalStateException("Model or Tokenizer file does not exist");
- }
-
- PoolingMode poolingMode = PoolingMode.MEAN;
- EmbeddingModel embeddingModel = new OnnxEmbeddingModel(modelFile.toPath(), tokenizerFile.toPath(), poolingMode);
- logger.info("Created OnnxEmbeddingModel with model: {} and tokenizer: {}", modelPath, tokenizerPath);
- return embeddingModel;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenAiEmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenAiEmbeddingCreator.java
deleted file mode 100644
index e0e80905..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenAiEmbeddingCreator.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
-
-import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
-import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-import dev.langchain4j.model.embedding.EmbeddingModel;
-import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
-
-/**
- * An embedding creator that uses OpenAI's embedding models for generating embeddings.
- * This class provides integration with OpenAI's embedding API, supporting high-throughput
- * embedding generation through parallel processing.
- *
- * Required environment variables:
- *
- * - {@code OPENAI_ORGANIZATION_ID}: Your OpenAI organization ID
- * - {@code OPENAI_API_KEY}: Your OpenAI API key
- *
- *
- * The default model used is "text-embedding-ada-002", but this can be overridden
- * through the configuration. The creator uses 40 threads by default for parallel
- * processing of embedding requests.
- */
-public class OpenAiEmbeddingCreator extends CachedEmbeddingCreator {
- /** Default number of threads for parallel processing */
- private static final int THREADS = 40;
-
- /**
- * Creates a new OpenAI embedding creator with the specified configuration.
- * The configuration can specify a custom model name, otherwise the default
- * "text-embedding-ada-002" is used.
- *
- * @param configuration The configuration containing model settings
- * @param contextStore The shared context store for pipeline components
- */
- public OpenAiEmbeddingCreator(ModuleConfiguration configuration, ContextStore contextStore) {
- super(contextStore, configuration.argumentAsString("model", "text-embedding-ada-002"), THREADS);
- }
-
- /**
- * Creates an OpenAI embedding model instance with the specified parameters.
- * The method requires both the organization ID and API key to be set in the
- * environment variables.
- *
- * @param model The name of the OpenAI model to use
- * @param params Additional parameters (not used in this implementation)
- * @return A configured OpenAI embedding model instance
- * @throws IllegalStateException If either OPENAI_ORGANIZATION_ID or OPENAI_API_KEY environment variable is not set
- */
- @Override
- protected EmbeddingModel createEmbeddingModel(String model, String... params) {
- String openAiOrganizationId = Environment.getenv("OPENAI_ORGANIZATION_ID");
- String openAiApiKey = Environment.getenv("OPENAI_API_KEY");
- if (openAiOrganizationId == null || openAiApiKey == null) {
- throw new IllegalStateException("OPENAI_ORGANIZATION_ID or OPENAI_API_KEY environment variable not set");
- }
- return new OpenAiEmbeddingModel.OpenAiEmbeddingModelBuilder()
- .modelName(model)
- .organizationId(openAiOrganizationId)
- .apiKey(openAiApiKey)
- .maxRetries(0)
- .build();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenWebUiEmbeddingCreator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenWebUiEmbeddingCreator.java
deleted file mode 100644
index 125d82e9..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenWebUiEmbeddingCreator.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
-
-import java.time.Duration;
-
-import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
-import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-import dev.langchain4j.model.embedding.EmbeddingModel;
-import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
-
-/**
- * An embedding creator that uses Open WebUI for generating embeddings.
- *
- * Required environment variables:
- *
- * - {@code OPENWEBUI_URL}: The URL of the Open WebUI server
- * - {@code OPENWEBUI_API_KEY}: API key for authentication
- *
- *
- * The default model used is "nomic-embed-text:v1.5", but this can be overridden
- * through the configuration.
- */
-public class OpenWebUiEmbeddingCreator extends CachedEmbeddingCreator {
-
- /**
- * Creates a new Open WebUI embedding creator with the specified configuration.
- * The configuration can specify a custom model name, otherwise the default
- * "nomic-embed-text:v1.5" is used.
- *
- * @param configuration The configuration containing model settings
- * @param contextStore The shared context store for pipeline components
- */
- public OpenWebUiEmbeddingCreator(ModuleConfiguration configuration, ContextStore contextStore) {
- super(contextStore, configuration.argumentAsString("model", "nomic-embed-text:v1.5"), 1);
- }
-
- /**
- * Creates an Open WebUI embedding model instance with the specified parameters.
- * The method configures the model with authentication if credentials are provided
- * in the environment variables.
- *
- * @param model The name of the Open WebUI model to use
- * @param params Additional parameters (not used in this implementation)
- * @return A configured Open WebUI embedding model instance
- */
- @Override
- protected EmbeddingModel createEmbeddingModel(String model, String... params) {
- String url = Environment.getenvNonNull("OPENWEBUI_URL");
- String apiKey = Environment.getenvNonNull("OPENWEBUI_API_KEY");
-
- var openWebUiEmbeddingModel = new OpenAiEmbeddingModel.OpenAiEmbeddingModelBuilder()
- .baseUrl(url)
- .apiKey(apiKey)
- .modelName(model)
- .timeout(Duration.ofMinutes(5));
- return openWebUiEmbeddingModel.build();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/SummarizePreprocessor.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/SummarizePreprocessor.java
index 3d72dde8..1cfb45b3 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/SummarizePreprocessor.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/SummarizePreprocessor.java
@@ -5,15 +5,15 @@
import java.util.List;
import java.util.concurrent.*;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.Cache;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.cache.Cache;
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager;
+import edu.kit.kastel.mcse.ardoco.llm.cache.chat.ChatCacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.util.Futures;
import edu.kit.kastel.sdq.lissa.ratlr.classifier.ChatLanguageModelProvider;
import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Artifact;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Futures;
import dev.langchain4j.model.chat.ChatModel;
@@ -51,7 +51,7 @@ public class SummarizePreprocessor extends Preprocessor {
/** Number of threads to use for parallel processing */
private final int threads;
/** Cache for storing and retrieving summaries */
- private final Cache cache;
+ private final Cache cache;
/**
* Creates a new summarize preprocessor with the specified configuration and context store.
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/IterativeOptimizer.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/IterativeOptimizer.java
index f74131a5..ed7eca07 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/IterativeOptimizer.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/IterativeOptimizer.java
@@ -15,9 +15,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.Cache;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.cache.Cache;
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager;
+import edu.kit.kastel.mcse.ardoco.llm.cache.chat.ChatCacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.chat.ChatModelUtils;
import edu.kit.kastel.sdq.lissa.ratlr.classifier.ChatLanguageModelProvider;
import edu.kit.kastel.sdq.lissa.ratlr.classifier.ClassificationTask;
import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
@@ -26,7 +27,6 @@
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.TraceLink;
import edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.promptmetric.Metric;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.ChatLanguageModelUtils;
import dev.langchain4j.model.chat.ChatModel;
@@ -124,7 +124,7 @@ public class IterativeOptimizer implements PromptOptimizer {
/**
* The cache used to store and retrieve prompt optimization LLM requests.
*/
- protected final Cache cache;
+ protected final Cache cache;
/**
* Provider for the language model used in classification.
@@ -258,7 +258,7 @@ protected String cachedSanitizedRequest(String request, int iteration) {
logger.debug("Sending request to LLM (iteration {})...", iteration);
logger.trace("Full LLM Request:\n{}", request);
- String response = ChatLanguageModelUtils.cachedRequest(request, llm, cache);
+ String response = ChatModelUtils.cachedRequest(request, llm, cache);
logger.debug("Received response from LLM (iteration {})", iteration);
logger.trace("Full LLM Response:\n{}", response);
@@ -278,7 +278,7 @@ protected String cachedSanitizedRequest(String request, int iteration) {
* @return The optimized prompt extracted from the response
*/
protected String cachedSanitizedRequest(String request) {
- String response = ChatLanguageModelUtils.cachedRequest(request, llm, cache);
+ String response = ChatModelUtils.cachedRequest(request, llm, cache);
return sanitizePrompt(parseTaggedTextFirst(response, PROMPT_START, PROMPT_END));
}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/ProTeGiOptimizer.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/ProTeGiOptimizer.java
index 39eae79d..1b349b3a 100644
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/ProTeGiOptimizer.java
+++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/ProTeGiOptimizer.java
@@ -1,10 +1,10 @@
/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer;
+import static edu.kit.kastel.mcse.ardoco.llm.chat.ChatModelUtils.nCachedRequest;
import static edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.PromptOptimizationUtils.getClassificationTasks;
import static edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.PromptOptimizationUtils.parseTaggedText;
import static edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.PromptOptimizationUtils.sanitizePrompts;
-import static edu.kit.kastel.sdq.lissa.ratlr.utils.ChatLanguageModelUtils.nCachedRequest;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/ChatLanguageModelUtils.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/ChatLanguageModelUtils.java
deleted file mode 100644
index 4400cb60..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/ChatLanguageModelUtils.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.utils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.Cache;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey;
-
-import dev.langchain4j.model.chat.ChatModel;
-
-/**
- * Utility class for interacting with chat-based language models,
- * providing methods for sending cached requests and handling multiple responses.
- */
-public final class ChatLanguageModelUtils {
-
- private static final Logger logger = LoggerFactory.getLogger(ChatLanguageModelUtils.class);
-
- private ChatLanguageModelUtils() {
- throw new IllegalAccessError("Utility class");
- }
-
- /**
- * Sends multiple requests to the language model and caches the responses.
- *
- * @param request The request to send to the language model
- * @param llm The chat model instance
- * @param cache The cache instance to use for caching responses
- * @param numberOfRequests The positive natural number of requests to send to the language model
- * @return A list of replies from the language model
- * @throws IllegalArgumentException If the number of requests is less than 1
- */
- public static List nCachedRequest(
- String request, ChatModel llm, Cache cache, int numberOfRequests) {
- if (numberOfRequests < 1) {
- throw new IllegalArgumentException("Number of requests must be at least 1");
- }
-
- String cacheKey = numberOfRequests + " results: \n" + request;
-
- List responses = cache.get(cacheKey, List.class);
- if (responses == null || responses.size() < numberOfRequests) {
- logger.debug("CACHE MISS - Making {} new LLM request(s)", numberOfRequests);
- responses = new ArrayList<>();
- logger.info("Optimizing with {} requests", numberOfRequests);
- for (int i = 1; i <= numberOfRequests; i++) {
- logger.debug(" Sending LLM request {}/{}", i, numberOfRequests);
- String response = llm.chat(request);
- logger.debug(" Received response {}/{} (length: {} chars)", i, numberOfRequests, response.length());
- responses.add(response);
- }
- cache.put(cacheKey, responses);
- logger.debug("Cached {} response(s) for future use", numberOfRequests);
- } else {
- logger.debug("CACHE HIT - Retrieved {} response(s) from cache", responses.size());
- }
- logger.debug("Responses: {}", responses);
- return responses;
- }
-
- /**
- * A wrapper for sending a single cached request to the language model using the
- * {@link #nCachedRequest(String, ChatModel, Cache, int)} method.
- *
- * @param request The request to send to the language model
- * @param llm The chat model instance
- * @param cache The cache instance to use for caching responses
- * @return The reply from the language model
- */
- public static String cachedRequest(String request, ChatModel llm, Cache cache) {
- return nCachedRequest(request, llm, cache, 1).getFirst();
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Environment.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Environment.java
deleted file mode 100644
index 02375f68..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Environment.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.utils;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import org.jspecify.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import io.github.cdimascio.dotenv.Dotenv;
-
-/**
- * A utility class for managing environment variables in the application.
- * This class provides functionality to:
- *
- * - Load environment variables from a .env file
- * - Fall back to system environment variables if .env is not available
- * - Retrieve environment variables with or without null checks
- *
- *
- * The class uses the following precedence for environment variables:
- *
- * - Values from the .env file (if it exists)
- * - Values from system environment variables
- *
- *
- * The .env file should be placed in the root directory of the project and should
- * contain key-value pairs in the format:
- *
- * KEY=value
- *
- */
-public final class Environment {
- private static final Logger logger = LoggerFactory.getLogger(Environment.class);
- /** The loaded .env configuration, or null if no .env file exists */
- private static volatile @Nullable Dotenv dotenv = load();
-
- private Environment() {
- throw new IllegalAccessError("Utility class");
- }
-
- /**
- * Retrieves an environment variable value.
- * This method:
- *
- * - First checks the .env file for the variable
- * - If not found, falls back to system environment variables
- * - Returns null if the variable is not found in either location
- *
- *
- * @param key The name of the environment variable to retrieve
- * @return The value of the environment variable, or null if not found
- */
- public static @Nullable String getenv(String key) {
- String dotenvValue = dotenv == null ? null : dotenv.get(key);
- if (dotenvValue != null) return dotenvValue;
- return System.getenv(key);
- }
-
- /**
- * Retrieves an environment variable value, requiring it to be non-null.
- * This method:
- *
- * - Attempts to retrieve the variable using {@link #getenv(String)}
- * - Throws an IllegalStateException if environment variable would be null
- *
- *
- * @param key The name of the environment variable to retrieve
- * @return The value of the environment variable
- * @throws IllegalStateException if the variable is not found and strict mode is enabled
- */
- public static String getenvNonNull(String key) {
- String env = getenv(key);
- if (env == null) {
- throw new IllegalStateException(
- "environment variable %s is missing, use '.env' or your system to set it up".formatted(key));
- }
- return env;
- }
-
- /**
- * Loads the .env file configuration.
- * This method:
- *
- * - Checks if a .env file exists in the project root
- * - If found, loads and returns the configuration
- * - If not found, logs a message and returns null
- *
- *
- * The method is synchronized to ensure thread safety during the initial loading.
- *
- * @return The loaded Dotenv configuration, or null if no .env file exists
- */
- private static synchronized @Nullable Dotenv load() {
- if (dotenv != null) {
- return dotenv;
- }
-
- if (Files.exists(Path.of(".env"))) {
- return Dotenv.configure().load();
- } else {
- logger.info("No .env file found, using system environment variables");
- return null;
- }
- }
-
- /**
- * Overwrites the current .env configuration with a new one from the specified path.
- * This method:
- *
- * - Checks if a .env file exists at the given path
- * - If found, loads and sets the new configuration
- * - If not found, logs a warning and retains the existing configuration
- *
- *
- * The method is synchronized to ensure thread safety when updating the configuration.
- *
- * @param path The path to the new .env file
- */
- public static synchronized void overwrite(Path path) {
- if (Files.exists(path)) {
- String directory;
- if (path.getParent() != null) {
- directory = path.getParent().toAbsolutePath().toString();
- } else {
- directory = Path.of("").toAbsolutePath().toString();
- }
-
- dotenv = Dotenv.configure()
- .directory(directory)
- .filename(path.getFileName().toString())
- .load();
- } else {
- logger.warn("No .env file found at '{}', using system environment variables", path);
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Futures.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Futures.java
deleted file mode 100644
index 42bf065e..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Futures.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.utils;
-
-import java.util.concurrent.Future;
-
-import org.slf4j.Logger;
-
-public final class Futures {
- private Futures() {
- throw new IllegalAccessError("Utility class");
- }
-
- @SuppressWarnings("java:S2139")
- public static T getLogged(Future future, Logger logger) {
- try {
- return future.get();
- } catch (InterruptedException e) {
- logger.error("Interrupted while waiting for future", e);
- Thread.currentThread().interrupt(); // Restore the interrupted status
- throw new IllegalStateException("Thread was interrupted while waiting for future result", e);
- } catch (Exception e) {
- logger.error("Error while getting future result: {}", e.getMessage(), e);
- throw new IllegalStateException(e);
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/KeyGenerator.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/KeyGenerator.java
deleted file mode 100644
index 904b8079..00000000
--- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/KeyGenerator.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* Licensed under MIT 2025. */
-package edu.kit.kastel.sdq.lissa.ratlr.utils;
-
-import java.nio.charset.StandardCharsets;
-import java.util.UUID;
-
-/**
- * A utility class for generating deterministic unique keys from input strings.
- * This class provides functionality to:
- *
- * - Generate deterministic UUIDs based on input strings
- * - Normalize line endings in input strings
- * - Create consistent keys for caching and identification purposes
- *
- *
- * The generated keys are:
- *
- * - Deterministic - same input always produces the same key
- * - Unique - different inputs produce different keys
- * - Normalized - line endings are standardized
- *
- */
-public final class KeyGenerator {
- /**
- * Private constructor to prevent instantiation of this utility class.
- */
- private KeyGenerator() {
- throw new IllegalAccessError("Utility class");
- }
-
- /**
- * Generates a deterministic UUID key from the given input string.
- * This method:
- *
- * - Normalizes line endings in the input string
- * - Converts the string into a UUID format
- *
- *
- * @param input The input string to generate a key from
- * @return A deterministic UUID based on the input string
- * @throws IllegalArgumentException if the input string is null
- */
- public static String generateKey(String input) {
- if (input == null) {
- throw new IllegalArgumentException("Input cannot be null");
- }
- // Normalize lineendings
- String normalized = input.replace("\r\n", "\n");
- return UUID.nameUUIDFromBytes(normalized.getBytes(StandardCharsets.UTF_8))
- .toString();
- }
-}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/ArchitectureTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/ArchitectureTest.java
index 89f4a658..065a8456 100644
--- a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/ArchitectureTest.java
+++ b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/ArchitectureTest.java
@@ -1,58 +1,36 @@
/* Licensed under MIT 2025-2026. */
package edu.kit.kastel.sdq.lissa.ratlr;
-import static com.tngtech.archunit.lang.SimpleConditionEvent.violated;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import java.nio.file.Path;
import java.util.List;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Future;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
-import com.tngtech.archunit.base.DescribedPredicate;
-import com.tngtech.archunit.core.domain.JavaClass;
-import com.tngtech.archunit.core.domain.JavaConstructorCall;
-import com.tngtech.archunit.core.domain.JavaMethod;
-import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
-import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
-import com.tngtech.archunit.lang.ConditionEvents;
-import com.tngtech.archunit.lang.SimpleConditionEvent;
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager;
+import edu.kit.kastel.mcse.ardoco.llm.util.Environment;
+import edu.kit.kastel.mcse.ardoco.llm.util.Futures;
+import edu.kit.kastel.mcse.ardoco.llm.util.KeyGenerator;
import edu.kit.kastel.sdq.lissa.cli.command.OptimizeCommand;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheKey;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheParameter;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheParameter;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.embedding.EmbeddingCacheParameter;
import edu.kit.kastel.sdq.lissa.ratlr.classifier.Classifier;
-import edu.kit.kastel.sdq.lissa.ratlr.classifier.LazyChatModel;
import edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.PromptOptimizer;
import edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.promptmetric.Metric;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Futures;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
-
-import dev.langchain4j.model.chat.ChatModel;
/**
* Architecture tests for the LiSSA framework using ArchUnit.
*
- * This class defines architectural rules to enforce:
- *
- * - Centralized environment variable access
- * - Centralized UUID generation
- * - Functional programming practices (avoid forEach for side effects)
- *
- * These rules help maintain code quality, consistency, and architectural integrity.
+ * These rules help maintain code quality, consistency, and architectural integrity. The invariants of the
+ * LLM/cache utility classes that were extracted into the {@code llm-access} library are checked by the
+ * companion {@link LlmAccessArchitectureTest}, which analyzes the library package that LiSSA depends on.
*/
@AnalyzeClasses(packages = "edu.kit.kastel.sdq.lissa")
class ArchitectureTest {
@@ -115,29 +93,6 @@ class ArchitectureTest {
.callMethod(List.class, "forEachOrdered", Consumer.class)
.because("Lambdas should be functional. ForEach is typically used for side-effects.");
- /**
- * Rule that enforces that CacheKey implementations should only be created via static factory methods.
- *
- * External code should not directly instantiate CacheKey implementations. Instead, they should use
- * the static factory methods (typically 'of()') provided by each CacheKey implementation or let the
- * CacheParameter.createCacheKey() method handle key creation.
- *
- * This rule checks that constructors of classes implementing CacheKey are not called from outside
- * those classes themselves (constructors are private and only called from static factory methods).
- */
- @ArchTest
- static final ArchRule cacheKeysShouldBeCreatedUsingKeyGenerator = noClasses()
- .that()
- .doNotImplement(CacheKey.class) // Exclude CacheKey implementations themselves
- .should()
- .callConstructorWhere(
- new DescribedPredicate("calls CacheKey implementation constructor") {
- @Override
- public boolean test(JavaConstructorCall javaConstructorCall) {
- return javaConstructorCall.getTarget().getOwner().isAssignableTo(CacheKey.class);
- }
- });
-
/**
* Prompts for classifiers should only be modified by optimizers or metric scorers. Otherwise, there will be
* inconsistencies with the configuration file.
@@ -174,190 +129,6 @@ public boolean test(JavaConstructorCall javaConstructorCall) {
.orShould()
.callMethod(Future.class, "resultNow");
- /**
- * Rule that enforces that each CacheKey implementation has a static of() method.
- *
- * Each class implementing CacheKey must provide a static factory method named 'of'
- * that takes a specific CacheParameter and a String as parameters. The method must
- * access all record components (accessor methods) of the corresponding CacheParameter.
- *
- * This ensures that all configuration parameters (model name, seed, temperature, etc.)
- * are properly used when creating cache keys, making the cache keys complete and unique.
- *
- * @see edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey#of(ClassifierCacheParameter, String)
- * @see edu.kit.kastel.sdq.lissa.ratlr.cache.embedding.EmbeddingCacheKey#of(EmbeddingCacheParameter, String)
- */
- @ArchTest
- static final ArchRule cacheKeysMustHaveOfMethodWithCacheParameter = classes()
- .that()
- .implement(CacheKey.class)
- .and()
- .areNotInterfaces()
- .should(
- new ArchCondition<>(
- "have a static 'of' method that takes a CacheParameter and String, and reads all CacheParameter attributes") {
- @Override
- public void check(JavaClass javaClass, ConditionEvents events) {
- // Check for static 'of' method
- var ofMethods = javaClass.getMethods().stream()
- .filter(m -> m.getName().equals("of"))
- .filter(m -> m.getModifiers().contains(JavaModifier.STATIC))
- .filter(m -> m.getRawParameterTypes().size() == 2)
- .filter(m -> m.getRawParameterTypes()
- .get(0)
- .isAssignableTo(edu.kit.kastel.sdq.lissa.ratlr.cache.CacheParameter.class))
- .filter(m -> m.getRawParameterTypes().get(1).isAssignableTo(String.class))
- .toList();
-
- if (ofMethods.isEmpty()) {
- String message = String.format(
- "Class %s does not have a static 'of' method with signature: of(CacheParameter, String)",
- javaClass.getFullName());
- events.add(violated(javaClass, message));
- return;
- }
-
- // Check that the 'of' method reads all CacheParameter attributes
- for (var ofMethod : ofMethods) {
- var cacheParameterType =
- ofMethod.getRawParameterTypes().get(0);
-
- // Get all accessor methods of the CacheParameter record components
- // Exclude inherited methods, utility methods, and factory methods
- var parameterMethods = cacheParameterType.getMethods().stream()
- .filter(m -> !m.getOwner().isEquivalentTo(Object.class))
- // parameters() generates cache file name, not used in key creation
- .filter(m -> !m.getName().equals("parameters"))
- // createCacheKey() is the factory method called by Cache, not by of()
- .filter(m -> !m.getName().equals("createCacheKey"))
- // Default methods from Object
- .filter(m -> !m.getName().equals("equals"))
- .filter(m -> !m.getName().equals("hashCode"))
- .filter(m -> !m.getName().equals("toString"))
- .toList();
-
- // Get all method calls in the 'of' method
- var methodCallsInOf = ofMethod.getMethodCallsFromSelf();
- Set calledMethodNames = methodCallsInOf.stream()
- .map(call -> call.getTarget().getName())
- .collect(Collectors.toSet());
-
- // Check if all parameter methods are called
- for (var paramMethod : parameterMethods) {
- boolean isCalled = calledMethodNames.contains(paramMethod.getName());
-
- if (!isCalled) {
- String message = String.format(
- "Method %s.of() does not read CacheParameter attribute '%s'",
- javaClass.getSimpleName(), paramMethod.getName());
- events.add(violated(javaClass, message));
- }
- }
- }
- }
- });
-
- /**
- * Rule that enforces that the parameters() method in each CacheParameter implementation accesses all fields.
- *
- * Each class implementing CacheParameter must have a parameters() method that uses all record components/fields
- * to ensure the cache key is unique and complete.
- */
- @ArchTest
- static final ArchRule cacheParametersMustUseAllFieldsInParametersMethod = classes()
- .that()
- .implement(CacheParameter.class)
- .and()
- .areNotInterfaces()
- .should(new ArchCondition<>("have a parameters() method that accesses all fields") {
- @Override
- public void check(JavaClass javaClass, ConditionEvents events) {
- // Find the parameters() method
- var parametersMethod = javaClass.getMethods().stream()
- .filter(m -> m.getName().equals("parameters"))
- .filter(m -> m.getRawParameterTypes().isEmpty())
- .findFirst();
-
- if (parametersMethod.isEmpty()) {
- String message =
- String.format("Class %s does not have a parameters() method", javaClass.getFullName());
- events.add(violated(javaClass, message));
- return;
- }
-
- // Get all fields of the CacheParameter (record components)
- var fields = javaClass.getAllFields().stream()
- .filter(f -> !f.getModifiers().contains(JavaModifier.STATIC))
- .toList();
-
- if (fields.isEmpty()) {
- return; // No fields to check
- }
-
- var method = parametersMethod.get();
-
- // Get all field accesses in the parameters() method
- var fieldAccesses = method.getFieldAccesses();
- Set accessedFieldNames = fieldAccesses.stream()
- .map(access -> access.getTarget().getName())
- .collect(Collectors.toSet());
-
- // Also check for method calls (record accessor methods)
- var methodCalls = method.getMethodCallsFromSelf();
- Set calledMethodNames = methodCalls.stream()
- .map(call -> call.getTarget().getName())
- .collect(Collectors.toSet());
-
- // Check if all fields are accessed (either directly or via accessor methods)
- for (var field : fields) {
- String fieldName = field.getName();
- boolean isAccessed =
- accessedFieldNames.contains(fieldName) || calledMethodNames.contains(fieldName);
-
- if (!isAccessed) {
- String message = String.format(
- "Method %s.parameters() does not access field '%s'",
- javaClass.getSimpleName(), fieldName);
- events.add(violated(javaClass, message));
- }
- }
- }
- });
-
- /**
- * Rule that enforces that LazyChatModel must override all methods declared in ChatModel, including default methods.
- *
- * This ensures that LazyChatModel provides its own implementation for all ChatModel methods, preventing accidental
- * usage of default implementations that may not be suitable for the lazy loading behavior of LazyChatModel.
- */
- @ArchTest
- static final ArchRule lazyChatModelMustOverrideAllChatModelMethods = classes()
- .that()
- .haveFullyQualifiedName(LazyChatModel.class.getName())
- .should()
- .implement(ChatModel.class)
- .andShould(new ArchCondition<>("override all methods declared in ChatModel (including default methods)") {
- @Override
- public void check(JavaClass clazz, ConditionEvents events) {
- JavaClass chatModel = clazz.getRawInterfaces().stream()
- .filter(i -> i.getName().equals(ChatModel.class.getName()))
- .findFirst()
- .orElseThrow();
-
- for (JavaMethod interfaceMethod : chatModel.getMethods()) {
- boolean overridden = clazz.getMethods().stream()
- .filter(m -> m.getOwner().equals(clazz)) // only methods declared in this class
- .anyMatch(m -> m.getName().equals(interfaceMethod.getName())
- && m.getRawParameterTypes().equals(interfaceMethod.getRawParameterTypes()));
-
- if (!overridden) {
- events.add(SimpleConditionEvent.violated(
- clazz, "Does not override method: " + interfaceMethod.getFullName()));
- }
- }
- }
- });
-
/**
* Rule that enforces that CacheManager.resetDefaultInstance() is only called from Test classes.
*
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/LlmAccessArchitectureTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/LlmAccessArchitectureTest.java
new file mode 100644
index 00000000..17d433a5
--- /dev/null
+++ b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/LlmAccessArchitectureTest.java
@@ -0,0 +1,251 @@
+/* Licensed under MIT 2025-2026. */
+package edu.kit.kastel.sdq.lissa.ratlr;
+
+import static com.tngtech.archunit.lang.SimpleConditionEvent.violated;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.tngtech.archunit.base.DescribedPredicate;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaConstructorCall;
+import com.tngtech.archunit.core.domain.JavaMethod;
+import com.tngtech.archunit.core.domain.JavaModifier;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.lang.SimpleConditionEvent;
+
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheKey;
+import edu.kit.kastel.mcse.ardoco.llm.cache.CacheParameter;
+import edu.kit.kastel.mcse.ardoco.llm.chat.LazyChatModel;
+
+import dev.langchain4j.model.chat.ChatModel;
+
+/**
+ * Architecture tests for the {@code llm-access} library that LiSSA depends on.
+ *
+ * The cache-key, cache-parameter and chat-model utilities that used to live in LiSSA now reside in the
+ * {@code llm-access} library. The structural invariants that guaranteed their correctness (complete cache
+ * keys, complete cache-file names, fully overridden lazy chat model) are just as important for LiSSA, which
+ * relies on those classes for caching correctness. These invariants are no longer covered by
+ * {@link ArchitectureTest}, so this class re-introduces the rules.
+ *
+ * The analysis covers both the library package (available on the test classpath as a dependency) and the
+ * LiSSA packages, so that LiSSA's own usage of these classes is checked as well (for example, LiSSA must not
+ * bypass the cache-key factory methods by constructing key implementations directly).
+ */
+@AnalyzeClasses(packages = {"edu.kit.kastel.mcse.ardoco.llm", "edu.kit.kastel.sdq.lissa"})
+class LlmAccessArchitectureTest {
+
+ /**
+ * Rule that enforces that {@link CacheKey} implementations are only created via their static factory
+ * methods.
+ *
+ * External code should not directly instantiate {@link CacheKey} implementations. Instead, they should
+ * use the static factory methods (typically {@code of()}) provided by each implementation or let the
+ * {@link CacheParameter#createCacheKey(String)} method handle key creation.
+ *
+ * This rule checks that constructors of classes implementing {@link CacheKey} are not called from
+ * outside those classes themselves (constructors are private and only called from static factory
+ * methods).
+ */
+ @ArchTest
+ static final ArchRule cacheKeysShouldBeCreatedUsingKeyGenerator = noClasses()
+ .that()
+ .doNotImplement(CacheKey.class) // Exclude CacheKey implementations themselves
+ .should()
+ .callConstructorWhere(
+ new DescribedPredicate("calls CacheKey implementation constructor") {
+ @Override
+ public boolean test(JavaConstructorCall javaConstructorCall) {
+ return javaConstructorCall.getTarget().getOwner().isAssignableTo(CacheKey.class);
+ }
+ });
+
+ /**
+ * Rule that enforces that each {@link CacheKey} implementation has a static {@code of()} method.
+ *
+ * Each class implementing {@link CacheKey} must provide a static factory method named {@code of}
+ * that takes a specific {@link CacheParameter} and a String as parameters. The method must
+ * access all record components (accessor methods) of the corresponding {@link CacheParameter}.
+ *
+ * This ensures that all configuration parameters (model name, seed, temperature, etc.)
+ * are properly used when creating cache keys, making the cache keys complete and unique.
+ */
+ @ArchTest
+ static final ArchRule cacheKeysMustHaveOfMethodWithCacheParameter = classes()
+ .that()
+ .implement(CacheKey.class)
+ .and()
+ .areNotInterfaces()
+ .should(
+ new ArchCondition<>(
+ "have a static 'of' method that takes a CacheParameter and String, and reads all CacheParameter attributes") {
+ @Override
+ public void check(JavaClass javaClass, ConditionEvents events) {
+ // Check for static 'of' method
+ var ofMethods = javaClass.getMethods().stream()
+ .filter(m -> m.getName().equals("of"))
+ .filter(m -> m.getModifiers().contains(JavaModifier.STATIC))
+ .filter(m -> m.getRawParameterTypes().size() == 2)
+ .filter(m -> m.getRawParameterTypes().get(0).isAssignableTo(CacheParameter.class))
+ .filter(m -> m.getRawParameterTypes().get(1).isAssignableTo(String.class))
+ .toList();
+
+ if (ofMethods.isEmpty()) {
+ String message = String.format(
+ "Class %s does not have a static 'of' method with signature: of(CacheParameter, String)",
+ javaClass.getFullName());
+ events.add(violated(javaClass, message));
+ return;
+ }
+
+ // Check that the 'of' method reads all CacheParameter attributes
+ for (var ofMethod : ofMethods) {
+ var cacheParameterType =
+ ofMethod.getRawParameterTypes().get(0);
+
+ // Get all accessor methods of the CacheParameter record components
+ // Exclude inherited methods, utility methods, and factory methods
+ var parameterMethods = cacheParameterType.getMethods().stream()
+ .filter(m -> !m.getOwner().isEquivalentTo(Object.class))
+ // parameters() generates cache file name, not used in key creation
+ .filter(m -> !m.getName().equals("parameters"))
+ // createCacheKey() is the factory method called by Cache, not by of()
+ .filter(m -> !m.getName().equals("createCacheKey"))
+ // Default methods from Object
+ .filter(m -> !m.getName().equals("equals"))
+ .filter(m -> !m.getName().equals("hashCode"))
+ .filter(m -> !m.getName().equals("toString"))
+ .toList();
+
+ // Get all method calls in the 'of' method
+ var methodCallsInOf = ofMethod.getMethodCallsFromSelf();
+ Set calledMethodNames = methodCallsInOf.stream()
+ .map(call -> call.getTarget().getName())
+ .collect(Collectors.toSet());
+
+ // Check if all parameter methods are called
+ for (var paramMethod : parameterMethods) {
+ boolean isCalled = calledMethodNames.contains(paramMethod.getName());
+
+ if (!isCalled) {
+ String message = String.format(
+ "Method %s.of() does not read CacheParameter attribute '%s'",
+ javaClass.getSimpleName(), paramMethod.getName());
+ events.add(violated(javaClass, message));
+ }
+ }
+ }
+ }
+ });
+
+ /**
+ * Rule that enforces that the {@code parameters()} method in each {@link CacheParameter} implementation
+ * accesses all fields.
+ *
+ * Each class implementing {@link CacheParameter} must have a {@code parameters()} method that uses all
+ * record components/fields to ensure the cache key is unique and complete.
+ */
+ @ArchTest
+ static final ArchRule cacheParametersMustUseAllFieldsInParametersMethod = classes()
+ .that()
+ .implement(CacheParameter.class)
+ .and()
+ .areNotInterfaces()
+ .should(new ArchCondition<>("have a parameters() method that accesses all fields") {
+ @Override
+ public void check(JavaClass javaClass, ConditionEvents events) {
+ // Find the parameters() method
+ var parametersMethod = javaClass.getMethods().stream()
+ .filter(m -> m.getName().equals("parameters"))
+ .filter(m -> m.getRawParameterTypes().isEmpty())
+ .findFirst();
+
+ if (parametersMethod.isEmpty()) {
+ String message =
+ String.format("Class %s does not have a parameters() method", javaClass.getFullName());
+ events.add(violated(javaClass, message));
+ return;
+ }
+
+ // Get all fields of the CacheParameter (record components)
+ var fields = javaClass.getAllFields().stream()
+ .filter(f -> !f.getModifiers().contains(JavaModifier.STATIC))
+ .toList();
+
+ if (fields.isEmpty()) {
+ return; // No fields to check
+ }
+
+ var method = parametersMethod.get();
+
+ // Get all field accesses in the parameters() method
+ var fieldAccesses = method.getFieldAccesses();
+ Set accessedFieldNames = fieldAccesses.stream()
+ .map(access -> access.getTarget().getName())
+ .collect(Collectors.toSet());
+
+ // Also check for method calls (record accessor methods)
+ var methodCalls = method.getMethodCallsFromSelf();
+ Set calledMethodNames = methodCalls.stream()
+ .map(call -> call.getTarget().getName())
+ .collect(Collectors.toSet());
+
+ // Check if all fields are accessed (either directly or via accessor methods)
+ for (var field : fields) {
+ String fieldName = field.getName();
+ boolean isAccessed =
+ accessedFieldNames.contains(fieldName) || calledMethodNames.contains(fieldName);
+
+ if (!isAccessed) {
+ String message = String.format(
+ "Method %s.parameters() does not access field '%s'",
+ javaClass.getSimpleName(), fieldName);
+ events.add(violated(javaClass, message));
+ }
+ }
+ }
+ });
+
+ /**
+ * Rule that enforces that {@link LazyChatModel} must override all methods declared in {@link ChatModel},
+ * including default methods.
+ *
+ * This ensures that {@link LazyChatModel} provides its own implementation for all {@link ChatModel}
+ * methods, preventing accidental usage of default implementations that may not be suitable for the lazy
+ * loading behavior of {@link LazyChatModel}.
+ */
+ @ArchTest
+ static final ArchRule lazyChatModelMustOverrideAllChatModelMethods = classes()
+ .that()
+ .haveFullyQualifiedName(LazyChatModel.class.getName())
+ .should()
+ .implement(ChatModel.class)
+ .andShould(new ArchCondition<>("override all methods declared in ChatModel (including default methods)") {
+ @Override
+ public void check(JavaClass clazz, ConditionEvents events) {
+ JavaClass chatModel = clazz.getRawInterfaces().stream()
+ .filter(i -> i.getName().equals(ChatModel.class.getName()))
+ .findFirst()
+ .orElseThrow();
+
+ for (JavaMethod interfaceMethod : chatModel.getMethods()) {
+ boolean overridden = clazz.getMethods().stream()
+ .filter(m -> m.getOwner().equals(clazz)) // only methods declared in this class
+ .anyMatch(m -> m.getName().equals(interfaceMethod.getName())
+ && m.getRawParameterTypes().equals(interfaceMethod.getRawParameterTypes()));
+
+ if (!overridden) {
+ events.add(SimpleConditionEvent.violated(
+ clazz, "Does not override method: " + interfaceMethod.getFullName()));
+ }
+ }
+ }
+ });
+}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategyTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategyTest.java
deleted file mode 100644
index 2fbadaeb..00000000
--- a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategyTest.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.nio.file.Path;
-import java.util.Objects;
-
-import org.jspecify.annotations.NullMarked;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
-
-/**
- * Comprehensive tests for CacheReplacementStrategy enum implementations.
- * These tests verify correct conflict resolution behavior with various data types
- * including strings, objects, and null values.
- */
-@NullMarked
-class CacheReplacementStrategyTest {
- private static final String TEST_KEY = "test-key";
- private static final String TEST_VALUE = "test-value";
- private static final TestObject TEST_OBJECT = new TestObject("test", 42);
- private static final String CONFLICTING_VALUE = "conflicting-value";
-
- @TempDir
- private Path tempCacheDir;
-
- private Cache primaryCache;
- private Cache secondaryCache;
- private TestCacheKey cacheKeyInstance;
-
- @BeforeEach
- void setUp() {
- primaryCache = createLocalCache("primary");
- secondaryCache = createLocalCache("secondary");
- cacheKeyInstance = TestCacheKey.of(new TestCacheParameter(), "test");
- }
-
- /**
- * Factory method to create a LocalCache instance for testing
- */
- private Cache createLocalCache(String cachePrefix) {
- return new LocalCache<>(
- tempCacheDir.resolve(cachePrefix + "_cache.json").toString(), new TestCacheParameter());
- }
-
- // ==================== NONE Strategy String Tests ====================
-
- @Test
- @DisplayName("NONE strategy: with string values - identical")
- void testNoneStrategyStringIdentical() {
- // Given both caches have identical values
- primaryCache.put(TEST_KEY, TEST_VALUE);
- secondaryCache.put(TEST_KEY, TEST_VALUE);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertEquals(TEST_VALUE, secondaryCache.get(TEST_KEY, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.NONE;
- String primaryValue = primaryCache.get(TEST_KEY, String.class);
- String secondaryValue = secondaryCache.get(TEST_KEY, String.class);
-
- // When resolving the values
- String result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then the primary value is returned and caches are unchanged
- assertEquals(TEST_VALUE, result);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertEquals(TEST_VALUE, secondaryCache.get(TEST_KEY, String.class));
- }
-
- @Test
- @DisplayName("NONE strategy: with string values - conflicting")
- void testNoneStrategyStringConflicting() {
- // Given primary and secondary have different values
- primaryCache.put(TEST_KEY, TEST_VALUE);
- secondaryCache.put(TEST_KEY, CONFLICTING_VALUE);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertEquals(CONFLICTING_VALUE, secondaryCache.get(TEST_KEY, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.NONE;
- String primaryValue = primaryCache.get(TEST_KEY, String.class);
- String secondaryValue = secondaryCache.get(TEST_KEY, String.class);
-
- // When resolving the conflict
- String result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then the primary value is returned and caches remain unchanged
- assertEquals(TEST_VALUE, result);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertEquals(CONFLICTING_VALUE, secondaryCache.get(TEST_KEY, String.class));
- }
-
- @Test
- @DisplayName("NONE strategy: with null primary")
- void testNoneStrategyNullPrimary() {
- // Given primary is null but secondary has a value
- secondaryCache.put(TEST_KEY, TEST_VALUE);
- assertNull(primaryCache.get(TEST_KEY, String.class));
- assertEquals(TEST_VALUE, secondaryCache.get(TEST_KEY, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.NONE;
- String primaryValue = primaryCache.get(TEST_KEY, String.class);
- String secondaryValue = secondaryCache.get(TEST_KEY, String.class);
-
- // When resolving with null primary
- String result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then the secondary value is backfilled to primary and returned
- assertEquals(TEST_VALUE, result);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertEquals(TEST_VALUE, secondaryCache.get(TEST_KEY, String.class));
- }
-
- @Test
- @DisplayName("NONE strategy: with object values - deep equal but different instances")
- void testNoneStrategyObjectDeepEqual() {
- // Given both caches have objects with same content but different instances
- TestObject obj1 = new TestObject("test", 42);
- TestObject obj2 = new TestObject("test", 42);
-
- primaryCache.put(TEST_KEY, obj1);
- secondaryCache.put(TEST_KEY, obj2);
- assertEquals(obj1, primaryCache.get(TEST_KEY, TestObject.class));
- assertEquals(obj2, secondaryCache.get(TEST_KEY, TestObject.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.NONE;
- TestObject primaryValue = primaryCache.get(TEST_KEY, TestObject.class);
- TestObject secondaryValue = secondaryCache.get(TEST_KEY, TestObject.class);
-
- // When resolving
- TestObject result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then primary value is returned (deep equal objects are considered same)
- assertEquals(obj1, result);
- assertEquals(obj2, result);
- }
-
- // ==================== ERROR Strategy Conflict Tests ====================
-
- @Test
- @DisplayName("ERROR strategy: throws when string values conflict")
- void testErrorStrategyStringConflict() {
- // Given primary and secondary have conflicting string values
- primaryCache.put(TEST_KEY, TEST_VALUE);
- secondaryCache.put(TEST_KEY, CONFLICTING_VALUE);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertEquals(CONFLICTING_VALUE, secondaryCache.get(TEST_KEY, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.ERROR;
- String primaryValue = primaryCache.get(TEST_KEY, String.class);
- String secondaryValue = secondaryCache.get(TEST_KEY, String.class);
-
- // When resolving conflicting values
- // Then an exception is thrown
- assertThrows(
- IllegalStateException.class,
- () -> strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache));
- }
-
- @Test
- @DisplayName("ERROR strategy: tolerates null vs non-null in different layers")
- void testErrorStrategyNullTolerance() {
- // Given primary has a value but secondary is null
- primaryCache.put(TEST_KEY, TEST_VALUE);
- assertEquals(TEST_VALUE, primaryCache.get(TEST_KEY, String.class));
- assertNull(secondaryCache.get(TEST_KEY, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.ERROR;
- String primaryValue = primaryCache.get(TEST_KEY, String.class);
- String secondaryValue = secondaryCache.get(TEST_KEY, String.class);
-
- // When resolving with one null value
- String result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then no exception is thrown and primary value is returned
- assertEquals(TEST_VALUE, result);
- }
-
- @Test
- @DisplayName("ERROR strategy: accepts identical objects")
- void testErrorStrategyIdenticalObjects() {
- // Given both caches have equal objects
- TestObject obj1 = new TestObject("test", 42);
- TestObject obj2 = new TestObject("test", 42);
-
- primaryCache.put(TEST_KEY, obj1);
- secondaryCache.put(TEST_KEY, obj2);
- assertEquals(obj1, primaryCache.get(TEST_KEY, TestObject.class));
- assertEquals(obj2, secondaryCache.get(TEST_KEY, TestObject.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.ERROR;
- TestObject primaryValue = primaryCache.get(TEST_KEY, TestObject.class);
- TestObject secondaryValue = secondaryCache.get(TEST_KEY, TestObject.class);
-
- // When resolving identical (deep equal) objects
- TestObject result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then no exception and primary value is returned
- assertEquals(obj1, result);
- }
-
- // ==================== OVERWRITE Strategy Tests ====================
-
- @Test
- @DisplayName("OVERWRITE strategy: overwrites secondary on conflict")
- void testOverwriteStrategyObjectConflict() {
- // Given secondary has a different value
- TestObject secondary = new TestObject("secondary", 2);
-
- primaryCache.put(TEST_KEY, TEST_OBJECT);
- secondaryCache.put(TEST_KEY, secondary);
- assertEquals(TEST_OBJECT, primaryCache.get(TEST_KEY, TestObject.class));
- assertEquals(secondary, secondaryCache.get(TEST_KEY, TestObject.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.OVERWRITE;
- TestObject primaryValue = primaryCache.get(TEST_KEY, TestObject.class);
- TestObject secondaryValue = secondaryCache.get(TEST_KEY, TestObject.class);
-
- // When resolving the conflict
- TestObject result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then primary value is used and secondary is overwritten
- assertEquals(TEST_OBJECT, result);
- assertEquals(TEST_OBJECT, secondaryCache.get(TEST_KEY, TestObject.class));
- }
-
- @Test
- @DisplayName("OVERWRITE strategy: does not overwrite when values are identical")
- void testOverwriteStrategyNoOverwriteOnIdentical() {
- // Given both caches have the same value
- primaryCache.put(TEST_KEY, TEST_OBJECT);
- secondaryCache.put(TEST_KEY, TEST_OBJECT);
- assertEquals(TEST_OBJECT, primaryCache.get(TEST_KEY, TestObject.class));
- assertEquals(TEST_OBJECT, secondaryCache.get(TEST_KEY, TestObject.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.OVERWRITE;
- TestObject primaryValue = primaryCache.get(TEST_KEY, TestObject.class);
- TestObject secondaryValue = secondaryCache.get(TEST_KEY, TestObject.class);
-
- // When resolving identical values
- TestObject result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then no overwrite occurs
- assertEquals(TEST_OBJECT, result);
- }
-
- @Test
- @DisplayName("OVERWRITE strategy: does backfill when secondary is null")
- void testOverwriteStrategyNoOverwriteWhenSecondaryNull() {
- // Given primary has value but secondary is empty
- primaryCache.put(TEST_KEY, TEST_OBJECT);
- assertEquals(TEST_OBJECT, primaryCache.get(TEST_KEY, TestObject.class));
- assertNull(secondaryCache.get(TEST_KEY, TestObject.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.OVERWRITE;
- TestObject primaryValue = primaryCache.get(TEST_KEY, TestObject.class);
- TestObject secondaryValue = secondaryCache.get(TEST_KEY, TestObject.class);
-
- // When resolving with null secondary
- TestObject result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then value is backfilled to secondary
- assertEquals(TEST_OBJECT, result);
- assertEquals(TEST_OBJECT, secondaryCache.get(TEST_KEY, TestObject.class));
- }
-
- @Test
- @DisplayName("OVERWRITE strategy: handles null primary with non-null secondary")
- void testOverwriteStrategyNullPrimary() {
- // Given secondary has value but primary is empty
- secondaryCache.put(TEST_KEY, TEST_OBJECT);
- assertNull(primaryCache.get(TEST_KEY, TestObject.class));
- assertEquals(TEST_OBJECT, secondaryCache.get(TEST_KEY, TestObject.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.OVERWRITE;
- TestObject primaryValue = primaryCache.get(TEST_KEY, TestObject.class);
- TestObject secondaryValue = secondaryCache.get(TEST_KEY, TestObject.class);
-
- // When resolving with null primary
- TestObject result = strategy.resolve(TEST_KEY, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then secondary value is backfilled to primary
- assertEquals(TEST_OBJECT, result);
- assertEquals(TEST_OBJECT, primaryCache.get(TEST_KEY, TestObject.class));
- }
-
- // ==================== ViaInternalKey Strategy Tests ====================
-
- @Test
- @DisplayName("NONE strategy via internal key: string backfill to primary when secondary has value")
- void testNoneStrategyViaInternalKeyStringBackfillPrimary() {
- // Given secondary has a string value but primary is null
- secondaryCache.putViaInternalKey(cacheKeyInstance, TEST_VALUE);
- assertNull(primaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- assertEquals(TEST_VALUE, secondaryCache.getViaInternalKey(cacheKeyInstance, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.NONE;
- String primaryValue = primaryCache.getViaInternalKey(cacheKeyInstance, String.class);
- String secondaryValue = secondaryCache.getViaInternalKey(cacheKeyInstance, String.class);
-
- // When resolving via internal key
- String result = strategy.resolveViaInternalKey(
- cacheKeyInstance, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then the secondary value is backfilled to primary using internal key
- assertEquals(TEST_VALUE, result);
- // Verify it was stored under the internal key, not the string representation of the key
- assertEquals(TEST_VALUE, primaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- assertEquals(TEST_VALUE, secondaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- }
-
- @Test
- @DisplayName("OVERWRITE strategy via internal key: string overwrite of secondary cache")
- void testOverwriteStrategyViaInternalKeyStringConflict() {
- // Given both caches have different string values
- primaryCache.putViaInternalKey(cacheKeyInstance, TEST_VALUE);
- secondaryCache.putViaInternalKey(cacheKeyInstance, CONFLICTING_VALUE);
- assertEquals(TEST_VALUE, primaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- assertEquals(CONFLICTING_VALUE, secondaryCache.getViaInternalKey(cacheKeyInstance, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.OVERWRITE;
- String primaryValue = primaryCache.getViaInternalKey(cacheKeyInstance, String.class);
- String secondaryValue = secondaryCache.getViaInternalKey(cacheKeyInstance, String.class);
-
- // When resolving via internal key
- String result = strategy.resolveViaInternalKey(
- cacheKeyInstance, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then primary value overwrites secondary via internal key
- assertEquals(TEST_VALUE, result);
- // Verify the secondary was updated with the internal key, not a converted string key
- assertEquals(TEST_VALUE, secondaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- }
-
- @Test
- @DisplayName("OVERWRITE strategy via internal key: string backfill to secondary when primary is null")
- void testOverwriteStrategyViaInternalKeyStringBackfillSecondary() {
- // Given primary is null but secondary has a string value
- secondaryCache.putViaInternalKey(cacheKeyInstance, TEST_VALUE);
- assertNull(primaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- assertEquals(TEST_VALUE, secondaryCache.getViaInternalKey(cacheKeyInstance, String.class));
-
- CacheReplacementStrategy strategy = CacheReplacementStrategy.OVERWRITE;
- String primaryValue = primaryCache.getViaInternalKey(cacheKeyInstance, String.class);
- String secondaryValue = secondaryCache.getViaInternalKey(cacheKeyInstance, String.class);
-
- // When resolving via internal key
- String result = strategy.resolveViaInternalKey(
- cacheKeyInstance, primaryValue, primaryCache, secondaryValue, secondaryCache);
-
- // Then the secondary value is backfilled to primary using internal key
- assertEquals(TEST_VALUE, result);
- assertEquals(TEST_VALUE, primaryCache.getViaInternalKey(cacheKeyInstance, String.class));
- }
- // ==================== Helper Classes ====================
-
- static class TestObject {
- public String name;
- public int value;
-
- @SuppressWarnings("unused")
- TestObject() {
- // For Jackson deserialization
- }
-
- TestObject(String name, int value) {
- this.name = name;
- this.value = value;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof TestObject that)) return false;
- return value == that.value && Objects.equals(name, that.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, value);
- }
-
- @Override
- public String toString() {
- return "TestObject{" + "name='" + name + '\'' + ", value=" + value + '}';
- }
- }
-
- static class TestCacheKey implements CacheKey {
- private final String keyValue;
-
- private TestCacheKey(String content) {
- this.keyValue = KeyGenerator.generateKey(content);
- }
-
- static TestCacheKey of(CacheParameter cacheParameter, String content) {
- // Access cacheParameter to satisfy architecture test requirements
- String parameters = cacheParameter.parameters();
- return new TestCacheKey(content + parameters);
- }
-
- @Override
- public String localKey() {
- return keyValue;
- }
-
- @Override
- public String toString() {
- return keyValue;
- }
- }
-
- static class TestCacheParameter implements CacheParameter {
- @Override
- public String parameters() {
- return "test-cache";
- }
-
- @Override
- public TestCacheKey createCacheKey(String content) {
- return TestCacheKey.of(this, content);
- }
- }
-}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheTest.java
deleted file mode 100644
index e2a8c6fe..00000000
--- a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import org.jspecify.annotations.NullMarked;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
-
-/**
- * Unit tests for LocalCache implementation.
- * These tests ensure that the local cache correctly persists and retrieves cache entries
- * while maintaining backward compatibility with existing cache files.
- */
-@NullMarked
-class CacheTest {
- @TempDir
- private Path tempCacheDir;
-
- @BeforeAll
- static void init() {
- Environment.overwrite(Path.of("src/test/resources/.env-test"));
- }
-
- @BeforeEach
- void setup() throws IOException {
- // Reset the default cache manager singleton for each test
- CacheManager.setCacheDir(tempCacheDir.toString());
- }
-
- @AfterEach
- void teardown() {
- // Clean up the cache manager after each test
- CacheManager.resetDefaultInstance();
- }
-
- @Test
- @DisplayName("New cache entries are written to cache file")
- void testWriteNewEntry() throws IOException {
- Cache cache = createLocalCache();
-
- cache.put("key1", "value1");
- cache.flush();
-
- Path cacheFile = tempCacheDir.resolve("test_cache.json");
- assertTrue(Files.exists(cacheFile));
- String content = Files.readString(cacheFile);
- assertTrue(content.contains("value1"));
- }
-
- @Test
- @DisplayName("Existing cache entries are retrieved from cache file")
- void testRetrieveExistingEntry() {
- Cache cache1 = createLocalCache();
- cache1.put("key1", "value1");
- cache1.flush();
-
- Cache cache2 = createLocalCache();
- String value = cache2.get("key1", String.class);
-
- assertEquals("value1", value);
- }
-
- @Test
- @DisplayName("Objects are serialized and deserialized correctly")
- void testObjectSerialization() {
- Cache cache = createLocalCache();
- TestObject obj = new TestObject("test", 42);
- cache.put("key1", obj);
- cache.flush();
-
- Cache cache2 = createLocalCache();
- TestObject retrieved = cache2.get("key1", TestObject.class);
-
- assertNotNull(retrieved);
- assertEquals("test", retrieved.name);
- assertEquals(42, retrieved.value);
- }
-
- @Test
- @DisplayName("Legacy cache files are backward compatible")
- void testBackwardCompatibility() throws IOException {
- Path sourceCacheFile = Path.of("src/test/resources/cache/test-local-cache-sample.json");
- Path cacheFile = tempCacheDir.resolve("test_cache.json");
- Files.copy(sourceCacheFile, cacheFile);
-
- Cache cache = createLocalCache();
- String value1 = cache.get("test-key-1", String.class);
- String value2 = cache.get("test-key-2", String.class);
- String value3 = cache.get("test-key-3", String.class);
-
- assertEquals("test-value-1", value1);
- assertEquals("test-value-2", value2);
- assertEquals("test-value-3", value3);
- }
-
- // Helper classes and methods
-
- /**
- * Simple test object for serialization/deserialization testing
- */
- static class TestObject {
- public String name = "";
- public int value;
-
- @SuppressWarnings("unused")
- TestObject() {
- // For Jackson deserialization
- }
-
- TestObject(String name, int value) {
- this.name = name;
- this.value = value;
- }
- }
-
- /**
- * Mock CacheKey implementation for testing
- */
- static class TestCacheKey implements CacheKey {
- private final String localKeyValue;
-
- private TestCacheKey(String content) {
- this.localKeyValue = KeyGenerator.generateKey(content);
- }
-
- @SuppressWarnings("unused")
- static TestCacheKey of(CacheParameter cacheParameter, String content) {
- return new TestCacheKey(content);
- }
-
- @Override
- public String localKey() {
- return localKeyValue;
- }
- }
-
- /**
- * Mock CacheParameter implementation for testing
- */
- static class TestCacheParameter implements CacheParameter {
- @Override
- public String parameters() {
- return "test-cache";
- }
-
- @Override
- public TestCacheKey createCacheKey(String content) {
- return TestCacheKey.of(this, content);
- }
- }
-
- /**
- * Factory method to create a LocalCache instance for testing
- */
- private Cache createLocalCache() {
- return new LocalCache<>(tempCacheDir.resolve("test_cache.json").toString(), new TestCacheParameter());
- }
-}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCacheTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCacheTest.java
deleted file mode 100644
index 50393975..00000000
--- a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCacheTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/* Licensed under MIT 2025-2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-import org.jspecify.annotations.NullMarked;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.mockito.junit.jupiter.MockitoSettings;
-import org.mockito.quality.Strictness;
-
-import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator;
-
-/**
- * Tests for HierarchicalCache layer synchronization behavior.
- * These tests verify that HierarchicalCache correctly synchronizes reads and writes across
- * multiple cache layers without requiring a real Redis or Docker instance.
- *
- * For tests of conflict resolution strategies, see {@link CacheReplacementStrategyTest}.
- */
-@NullMarked
-@MockitoSettings(strictness = Strictness.LENIENT)
-@ExtendWith(MockitoExtension.class)
-class HierarchicalCacheTest {
- private static final String TEST_KEY = "test-key";
-
- @Mock
- private Cache primaryCache;
-
- @Mock
- private Cache secondaryCache;
-
- @Mock
- private CacheParameter cacheParameter;
-
- private TestCacheKey cacheKeyInstance;
-
- @BeforeEach
- void setUp() {
- cacheKeyInstance = TestCacheKey.of(cacheParameter, "test");
- when(cacheParameter.createCacheKey(anyString())).thenReturn(cacheKeyInstance);
- }
-
- @Test
- @DisplayName("put() writes to both primary and secondary cache")
- void testPutObjectWritesToBothCaches() {
- HierarchicalCache cache =
- new HierarchicalCache<>(cacheParameter, primaryCache, secondaryCache, CacheReplacementStrategy.NONE);
-
- TestObject testObj = new TestObject("test", 42);
- cache.put(TEST_KEY, testObj);
-
- verify(primaryCache).put(eq(TEST_KEY), same(testObj));
- verify(secondaryCache).put(eq(TEST_KEY), same(testObj));
- }
-
- @Test
- @DisplayName("containsKey() returns true if primary cache contains key")
- void testContainsKeyInPrimary() {
- HierarchicalCache cache =
- new HierarchicalCache<>(cacheParameter, primaryCache, secondaryCache, CacheReplacementStrategy.NONE);
-
- when(primaryCache.containsKey(TEST_KEY)).thenReturn(true);
- when(secondaryCache.containsKey(TEST_KEY)).thenReturn(false);
-
- assertTrue(cache.containsKey(TEST_KEY));
- }
-
- @Test
- @DisplayName("containsKey() returns true if secondary cache contains key")
- void testContainsKeyInSecondary() {
- HierarchicalCache cache =
- new HierarchicalCache<>(cacheParameter, primaryCache, secondaryCache, CacheReplacementStrategy.NONE);
-
- when(primaryCache.containsKey(TEST_KEY)).thenReturn(false);
- when(secondaryCache.containsKey(TEST_KEY)).thenReturn(true);
-
- assertTrue(cache.containsKey(TEST_KEY));
- }
-
- @Test
- @DisplayName("containsKey() returns false if neither cache contains key")
- void testContainsKeyInNeither() {
- HierarchicalCache cache =
- new HierarchicalCache<>(cacheParameter, primaryCache, secondaryCache, CacheReplacementStrategy.NONE);
-
- when(primaryCache.containsKey(TEST_KEY)).thenReturn(false);
- when(secondaryCache.containsKey(TEST_KEY)).thenReturn(false);
-
- assertFalse(cache.containsKey(TEST_KEY));
- }
-
- @Test
- @DisplayName("flush() flushes both caches")
- void testFlushBothCaches() {
- HierarchicalCache cache =
- new HierarchicalCache<>(cacheParameter, primaryCache, secondaryCache, CacheReplacementStrategy.NONE);
-
- cache.flush();
-
- verify(primaryCache).flush();
- verify(secondaryCache).flush();
- }
-
- // ==================== Helper Classes ====================
-
- static class TestObject {
- public String name;
- public int value;
-
- @SuppressWarnings("unused")
- TestObject() {
- // For Jackson deserialization
- }
-
- TestObject(String name, int value) {
- this.name = name;
- this.value = value;
- }
- }
-
- static class TestCacheKey implements CacheKey {
- private final String keyValue;
-
- private TestCacheKey(String content) {
- this.keyValue = KeyGenerator.generateKey(content);
- }
-
- static TestCacheKey of(CacheParameter cacheParameter, String content) {
- // Access cacheParameter to satisfy architecture test requirements
- String parameters = cacheParameter.parameters();
- return new TestCacheKey(content + parameters);
- }
-
- @Override
- public String toJsonKey() {
- return keyValue;
- }
-
- @Override
- public String localKey() {
- return keyValue;
- }
- }
-}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisTest.java
deleted file mode 100644
index 4b007a2a..00000000
--- a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisTest.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/* Licensed under MIT 2026. */
-package edu.kit.kastel.sdq.lissa.ratlr.cache;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import org.fuchss.restredis.client.Client;
-import org.fuchss.restredis.client.ClientConfiguration;
-import org.fuchss.restredis.server.Server;
-import org.fuchss.restredis.server.ServerConfiguration;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey;
-import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheParameter;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
-
-import kong.unirest.core.Unirest;
-
-/**
- * Integration test for the REST Redis interface, using a Testcontainers-managed Redis instance.
- */
-@Testcontainers
-public class RestRedisTest {
-
- private static final Path BASELINE_ENV = Path.of("src/test/resources/.env-test");
-
- @Container
- private static final GenericContainer> REDIS =
- new GenericContainer<>(DockerImageName.parse("redis:latest")).withExposedPorts(6379);
-
- RestRedisCache restCache;
- private final ClassifierCacheParameter cacheParameter = new ClassifierCacheParameter("test", 1, 0.0);
-
- private static Path envFile;
- private static Thread serverThread;
- private static Client client;
-
- @TempDir
- private static Path tempCacheDir;
-
- @BeforeAll
- static void startServer() throws Exception {
- int httpPort = findFreePort();
- Path configFile = tempCacheDir.resolve("server_config.json");
- new ObjectMapper()
- .writeValue(
- configFile.toFile(),
- new ServerConfiguration(REDIS.getHost(), REDIS.getMappedPort(6379), httpPort));
-
- serverThread = new Thread(
- () -> {
- try {
- Server.main(new String[] {configFile.toString()});
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- },
- "rest-redis-test-server");
- serverThread.setDaemon(true);
- serverThread.start();
-
- String baseUrl = "http://localhost:" + httpPort;
- waitForServerReady(baseUrl);
- envFile = tempCacheDir.resolve(".env-rest");
-
- Files.writeString(envFile, """
- REST_REDIS_URI=%s
- REST_REDIS_USERNAME=
- REST_REDIS_PASSWORD=
- """.formatted(baseUrl));
-
- Environment.overwrite(envFile);
- client = new Client(new ClientConfiguration(baseUrl, null, null));
- }
-
- @AfterAll
- static void stopServer() throws InterruptedException {
- if (client != null) {
- client.close();
- }
- if (serverThread != null) {
- serverThread.interrupt();
- serverThread.join(5000);
- }
- Environment.overwrite(BASELINE_ENV);
- Unirest.shutDown();
- }
-
- @BeforeEach
- public void setup() {
- Environment.overwrite(envFile);
- restCache = new RestRedisCache<>(cacheParameter, new ObjectMapper());
- }
-
- /**
- * Tests that a connection to the redis client can be established.
- */
- @Test
- @DisplayName("Test REST Redis client connection")
- void testRestRedisConnection() {
- Cache.createByType(
- CacheType.REST_REDIS, new ClassifierCacheParameter("test", 1, 0.0), null, new ObjectMapper());
- }
-
- /**
- * Tests that the REST Redis cache can successfully set and get values, and that it returns null for non-existing keys.
- */
- @Test
- @DisplayName("Test REST Redis cache set and get")
- void testRestRedisCacheSetAndGet() {
- restCache.put("key", "value");
- String value = restCache.get("key", String.class);
- assertEquals("value", value);
- String nonExistingValue = restCache.get("ajhosadljhjyhxcjkhljysdhjk", String.class);
- assertNull(nonExistingValue);
- }
-
- /**
- * Tests that the hierarchical cache correctly handles conflicts between a local file cache and a REST Redis cache
- * when using the NONE strategy, ensuring that the primary cache value is returned and the secondary cache remains
- * unchanged.
- */
- @Test
- @DisplayName("Test HierarchicalCache with local and REST Redis cache")
- void testHierarchicalCacheWithLocalAndRestRedis() {
- // Given: Create local and REST Redis caches
- Cache localCache =
- new LocalCache<>(tempCacheDir.resolve("hierarchical_test.json").toString(), cacheParameter);
- Cache redisCache = new RestRedisCache<>(cacheParameter, new ObjectMapper());
-
- // When: Direct writes to separate caches create a conflict
- String testKey = "conflict-key";
- String localValue = "local-value";
- String redisValue = "redis-value";
-
- localCache.put(testKey, localValue);
- redisCache.put(testKey, redisValue);
-
- // Create hierarchical cache with NONE strategy (returns primary value, backfills missing)
- HierarchicalCache hierarchicalCacheNone =
- new HierarchicalCache<>(cacheParameter, localCache, redisCache, CacheReplacementStrategy.NONE);
-
- // Then: NONE strategy returns primary (local) value
- String result = hierarchicalCacheNone.get(testKey, String.class);
- assertEquals(localValue, result);
-
- // And: Secondary cache remains unchanged
- assertEquals(redisValue, redisCache.get(testKey, String.class));
- }
-
- /**
- * Tests that the overwrite strategy correctly overwrites the secondary REST Redis cache with the primary local
- * cache value when there is a conflict.
- */
- @Test
- @DisplayName("Test HierarchicalCache OVERWRITE strategy with REST Redis")
- void testHierarchicalCacheOverwriteStrategyWithRestRedis() {
- // Given: Create local and REST Redis caches
- Cache localCache =
- new LocalCache<>(tempCacheDir.resolve("overwrite_test.json").toString(), cacheParameter);
- Cache redisCache = new RestRedisCache<>(cacheParameter, new ObjectMapper());
-
- // When: Cache layers have conflicting values
- String testKey = "overwrite-key";
- String primaryValue = "primary-value";
- String secondaryValue = "secondary-value";
-
- localCache.put(testKey, primaryValue);
- redisCache.put(testKey, secondaryValue);
-
- // Create hierarchical cache with OVERWRITE strategy
- HierarchicalCache hierarchicalCacheOverwrite =
- new HierarchicalCache<>(cacheParameter, localCache, redisCache, CacheReplacementStrategy.OVERWRITE);
-
- // When: Get the value through hierarchical cache
- String result = hierarchicalCacheOverwrite.get(testKey, String.class);
-
- // Then: Primary value is returned
- assertEquals(primaryValue, result);
-
- // And: Secondary (REST Redis) cache is overwritten with primary value
- hierarchicalCacheOverwrite.flush();
- assertEquals(primaryValue, redisCache.get(testKey, String.class));
- }
-
- /**
- * Tests the error strategy for conflicting values in the remote REST cache and local file cache.
- */
- @Test
- @DisplayName("Test HierarchicalCache ERROR strategy detects conflicts with REST Redis")
- void testHierarchicalCacheErrorStrategyWithRestRedis() {
- // Given: Create local and REST Redis caches
- Cache localCache =
- new LocalCache<>(tempCacheDir.resolve("error_test.json").toString(), cacheParameter);
- Cache redisCache = new RestRedisCache<>(cacheParameter, new ObjectMapper());
-
- // When: Cache layers have conflicting values
- String testKey = "error-key";
- String localValue = "local-value";
- String redisValue = "different-redis-value";
-
- localCache.put(testKey, localValue);
- redisCache.put(testKey, redisValue);
-
- // Create hierarchical cache with ERROR strategy
- HierarchicalCache hierarchicalCacheError =
- new HierarchicalCache<>(cacheParameter, localCache, redisCache, CacheReplacementStrategy.ERROR);
-
- // Then: Getting conflicting values throws an exception
- assertTrue(Assertions.assertThrows(
- IllegalStateException.class, () -> hierarchicalCacheError.get(testKey, String.class))
- .getMessage()
- .contains("Cache inconsistency"));
- }
-
- /**
- * Tests backfilling from REST Redis to local cache when primary cache is missing a value.
- */
- @Test
- @DisplayName("Test HierarchicalCache backfill with REST Redis cache")
- void testHierarchicalCacheBackfillWithRestRedis() {
- // Given: Create local and REST Redis caches
- Cache localCache =
- new LocalCache<>(tempCacheDir.resolve("backfill_test.json").toString(), cacheParameter);
- Cache redisCache = new RestRedisCache<>(cacheParameter, new ObjectMapper());
-
- // When: Only secondary (REST Redis) has a value
- String testKey = "backfill-key";
- String redisValue = "redis-only-value";
- redisCache.put(testKey, redisValue);
-
- assertNull(localCache.get(testKey, String.class));
-
- // Create hierarchical cache with NONE strategy (backfills primary from secondary)
- HierarchicalCache hierarchicalCache =
- new HierarchicalCache<>(cacheParameter, localCache, redisCache, CacheReplacementStrategy.NONE);
-
- // When: Get the value
- String result = hierarchicalCache.get(testKey, String.class);
-
- // Then: Value from secondary cache is returned
- assertEquals(redisValue, result);
-
- // And: Primary cache is backfilled with the value
- hierarchicalCache.flush();
- assertEquals(redisValue, localCache.get(testKey, String.class));
- }
-
- private static int findFreePort() throws IOException {
- try (ServerSocket socket = new ServerSocket(0)) {
- return socket.getLocalPort();
- }
- }
-
- private static void waitForServerReady(String baseUrl) throws InterruptedException {
- long deadline = System.currentTimeMillis() + 30000;
- while (System.currentTimeMillis() < deadline) {
- if (isServerResponding(baseUrl)) {
- return;
- }
- Thread.sleep(150);
- }
- throw new IllegalStateException("REST-Redis server did not become ready in time");
- }
-
- private static boolean isServerResponding(String baseUrl) {
- try {
- var response = Unirest.get(baseUrl + "/").asString();
- return response.getStatus() >= 200 && response.getStatus() < 600;
- } catch (Exception e) {
- return false;
- }
- }
-}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/MockPipelineE2ETest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/MockPipelineE2ETest.java
new file mode 100644
index 00000000..055c4a75
--- /dev/null
+++ b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/MockPipelineE2ETest.java
@@ -0,0 +1,42 @@
+/* Licensed under MIT 2025-2026. */
+package edu.kit.kastel.sdq.lissa.ratlr.e2e;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Set;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import edu.kit.kastel.mcse.ardoco.llm.util.Environment;
+import edu.kit.kastel.sdq.lissa.ratlr.Evaluation;
+import edu.kit.kastel.sdq.lissa.ratlr.knowledge.TraceLink;
+
+/**
+ * Runs the full LiSSA pipeline in mock mode (mock embeddings + mock classifier). This exercises the whole
+ * pipeline offline, without any model access. The mock classifier links every retrieved candidate, so the
+ * run must produce trace links.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class MockPipelineE2ETest {
+
+ @BeforeAll
+ void init() {
+ Environment.overwrite(Path.of("src/test/resources/.env-test"));
+ }
+
+ @Test
+ void testMockPipelineRuns() throws Exception {
+ File config = new File("src/test/resources/warc/config-mock.json");
+ Assertions.assertTrue(config.exists(), "mock config missing at " + config.getAbsolutePath());
+
+ Evaluation evaluation = new Evaluation(config.toPath());
+ Set traceLinks = evaluation.run();
+
+ Assertions.assertNotNull(traceLinks);
+ Assertions.assertFalse(
+ traceLinks.isEmpty(), "the mock classifier links every retrieved candidate, so links must exist");
+ }
+}
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/Requirement2RequirementE2ETest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/Requirement2RequirementE2ETest.java
index 7a4298b0..2268e350 100644
--- a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/Requirement2RequirementE2ETest.java
+++ b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/Requirement2RequirementE2ETest.java
@@ -18,11 +18,11 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import edu.kit.kastel.mcse.ardoco.llm.util.Environment;
import edu.kit.kastel.mcse.ardoco.metrics.ClassificationMetricsCalculator;
import edu.kit.kastel.sdq.lissa.ratlr.Evaluation;
import edu.kit.kastel.sdq.lissa.ratlr.Optimization;
import edu.kit.kastel.sdq.lissa.ratlr.knowledge.TraceLink;
-import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment;
class Requirement2RequirementE2ETest {
diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreatorTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreatorTest.java
new file mode 100644
index 00000000..284d7d06
--- /dev/null
+++ b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreatorTest.java
@@ -0,0 +1,38 @@
+/* Licensed under MIT 2025-2026. */
+package edu.kit.kastel.sdq.lissa.ratlr.embeddingcreator;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration;
+import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore;
+import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element;
+
+/**
+ * Tests the mock embedding configuration: it needs no {@code model} argument and produces zero vectors.
+ */
+class EmbeddingCreatorTest {
+
+ private Element element(String id, String content) {
+ return new Element(id, "requirement", content, 0, null, true);
+ }
+
+ @Test
+ void mockEmbeddingConfigurationNeedsNoModelAndReturnsZeroVectors() {
+ // No "model" argument: the mock creator ignores it, so none is required (and a stray one would be
+ // reported as an unread parameter by ModuleConfiguration).
+ ModuleConfiguration configuration = new ModuleConfiguration("mock", Map.of());
+ EmbeddingCreator creator = EmbeddingCreator.createEmbeddingCreator(configuration, new ContextStore());
+
+ Assertions.assertArrayEquals(
+ new float[] {0}, creator.calculateEmbedding(element("R1", "the system shall log in")));
+
+ List embeddings = creator.calculateEmbeddings(List.of(element("R1", "a"), element("R2", "b")));
+ Assertions.assertEquals(2, embeddings.size());
+ Assertions.assertArrayEquals(new float[] {0}, embeddings.get(0));
+ Assertions.assertArrayEquals(new float[] {0}, embeddings.get(1));
+ }
+}
diff --git a/src/test/resources/warc/config-mock.json b/src/test/resources/warc/config-mock.json
new file mode 100644
index 00000000..edd89136
--- /dev/null
+++ b/src/test/resources/warc/config-mock.json
@@ -0,0 +1,57 @@
+{
+ "cache_dir": "./target/lissa-mock-cache",
+
+ "gold_standard_configuration": {
+ "path": "./src/test/resources/warc/answer.csv",
+ "hasHeader": "true"
+ },
+
+ "source_artifact_provider" : {
+ "name" : "text",
+ "args" : {
+ "artifact_type" : "requirement",
+ "path" : "./src/test/resources/warc/high"
+ }
+ },
+ "target_artifact_provider" : {
+ "name" : "text",
+ "args" : {
+ "artifact_type" : "requirement",
+ "path" : "./src/test/resources/warc/low"
+ }
+ },
+ "source_preprocessor" : {
+ "name" : "artifact",
+ "args" : {}
+ },
+ "target_preprocessor" : {
+ "name" : "artifact",
+ "args" : {}
+ },
+ "embedding_creator" : {
+ "name" : "mock",
+ "args" : {}
+ },
+ "source_store" : {
+ "name" : "custom",
+ "args" : { }
+ },
+ "target_store" : {
+ "name" : "custom",
+ "args" : {
+ "max_results" : "4"
+ }
+ },
+ "classifier" : {
+ "name" : "mock",
+ "args" : {}
+ },
+ "result_aggregator" : {
+ "name" : "any_connection",
+ "args" : {}
+ },
+ "tracelinkid_postprocessor" : {
+ "name" : "identity",
+ "args" : {}
+ }
+}