From 1b58f1b5a75292953d68a39628a38ee8411c3aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Sun, 2 Aug 2026 01:07:10 +0200 Subject: [PATCH 1/6] Switch to llm-access library --- pom.xml | 25 +- .../cli/command/TransitiveTraceCommand.java | 2 +- .../kastel/sdq/lissa/ratlr/Evaluation.java | 2 +- .../edu/kit/kastel/sdq/lissa/ratlr/Main.java | 2 +- .../kastel/sdq/lissa/ratlr/Optimization.java | 2 +- .../kastel/sdq/lissa/ratlr/cache/Cache.java | 146 ------ .../sdq/lissa/ratlr/cache/CacheKey.java | 47 -- .../sdq/lissa/ratlr/cache/CacheManager.java | 277 ------------ .../sdq/lissa/ratlr/cache/CacheParameter.java | 28 -- .../ratlr/cache/CacheReplacementStrategy.java | 193 -------- .../sdq/lissa/ratlr/cache/CacheType.java | 20 - .../lissa/ratlr/cache/HierarchicalCache.java | 112 ----- .../cache/LargeLanguageModelCacheMode.java | 17 - .../sdq/lissa/ratlr/cache/LocalCache.java | 204 --------- .../sdq/lissa/ratlr/cache/RedisAdapter.java | 77 ---- .../sdq/lissa/ratlr/cache/RedisCache.java | 164 ------- .../lissa/ratlr/cache/RestRedisAdapter.java | 84 ---- .../sdq/lissa/ratlr/cache/RestRedisCache.java | 64 --- .../lissa/ratlr/cache/UnifiedRedisClient.java | 52 --- .../cache/classifier/ClassifierCacheKey.java | 141 ------ .../classifier/ClassifierCacheParameter.java | 31 -- .../cache/embedding/EmbeddingCacheKey.java | 139 ------ .../embedding/EmbeddingCacheParameter.java | 23 - .../classifier/ChatLanguageModelPlatform.java | 92 ---- .../classifier/ChatLanguageModelProvider.java | 295 ++---------- .../lissa/ratlr/classifier/Classifier.java | 2 +- .../lissa/ratlr/classifier/LazyChatModel.java | 103 ----- .../ratlr/classifier/PipelineClassifier.java | 2 +- .../ratlr/classifier/ReasoningClassifier.java | 10 +- .../ratlr/classifier/SimpleClassifier.java | 10 +- .../ratlr/configuration/Configuration.java | 2 +- .../ratlr/elementstore/ElementStore.java | 4 +- .../elementstore/ElementStoreOperations.java | 2 +- .../elementstore/SourceElementStore.java | 4 +- .../elementstore/TargetElementStore.java | 4 +- .../strategy/CosineSimilarity.java | 2 +- .../strategy/RetrievalStrategy.java | 2 +- .../CachedEmbeddingCreator.java | 264 ----------- .../embeddingcreator/EmbeddingCreator.java | 128 ++---- .../MockEmbeddingCreator.java | 40 -- .../OllamaEmbeddingCreator.java | 73 --- .../OnnxEmbeddingCreator.java | 83 ---- .../OpenAiEmbeddingCreator.java | 66 --- .../OpenWebUiEmbeddingCreator.java | 60 --- .../preprocessor/SummarizePreprocessor.java | 10 +- .../promptoptimizer/IterativeOptimizer.java | 14 +- .../promptoptimizer/ProTeGiOptimizer.java | 4 +- .../PromptOptimizationUtils.java | 2 +- .../ratlr/utils/ChatLanguageModelUtils.java | 77 ---- .../sdq/lissa/ratlr/utils/Environment.java | 138 ------ .../kastel/sdq/lissa/ratlr/utils/Futures.java | 26 -- .../sdq/lissa/ratlr/utils/KeyGenerator.java | 52 --- .../kastel/sdq/lissa/ratlr/utils/Pair.java | 12 - .../sdq/lissa/ratlr/ArchitectureTest.java | 243 +--------- .../cache/CacheReplacementStrategyTest.java | 428 ------------------ .../sdq/lissa/ratlr/cache/CacheTest.java | 170 ------- .../ratlr/cache/HierarchicalCacheTest.java | 151 ------ .../sdq/lissa/ratlr/cache/RestRedisTest.java | 292 ------------ .../e2e/Requirement2RequirementE2ETest.java | 2 +- 59 files changed, 147 insertions(+), 4574 deletions(-) delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheKey.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheManager.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheParameter.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategy.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheType.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCache.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LargeLanguageModelCacheMode.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LocalCache.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheKey.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheParameter.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheKey.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheParameter.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/ChatLanguageModelPlatform.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/LazyChatModel.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/CachedEmbeddingCreator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/MockEmbeddingCreator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OllamaEmbeddingCreator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OnnxEmbeddingCreator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenAiEmbeddingCreator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenWebUiEmbeddingCreator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/ChatLanguageModelUtils.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Environment.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Futures.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/KeyGenerator.java delete mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java delete mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheReplacementStrategyTest.java delete mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheTest.java delete mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCacheTest.java delete mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisTest.java diff --git a/pom.xml b/pom.xml index 9fece3bc..5e443022 100644 --- a/pom.xml +++ b/pom.xml @@ -52,11 +52,6 @@ com.fasterxml.jackson.core jackson-databind - - com.knuddels - jtokkit - 1.1.0 - com.tngtech.archunit archunit-junit5 @@ -88,6 +83,11 @@ picocli ${picocli.version} + + io.github.ardoco + llm-access + 0.1.0-SNAPSHOT + io.github.ardoco metrics @@ -109,22 +109,12 @@ tree-sitter-java 0.23.5 - - io.github.cdimascio - dotenv-java - 3.2.0 - io.soabase.record-builder record-builder-processor ${record-builder.version} provided - - org.fuchss - rest-redis - 0.1.4 - org.junit.jupiter junit-jupiter-engine @@ -155,11 +145,6 @@ testcontainers-junit-jupiter test - - redis.clients - jedis - 7.5.2 - diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/cli/command/TransitiveTraceCommand.java b/src/main/java/edu/kit/kastel/sdq/lissa/cli/command/TransitiveTraceCommand.java index 75cbea38..bcfc726e 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/cli/command/TransitiveTraceCommand.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/cli/command/TransitiveTraceCommand.java @@ -10,12 +10,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.llm.util.KeyGenerator; import edu.kit.kastel.sdq.lissa.ratlr.Evaluation; import edu.kit.kastel.sdq.lissa.ratlr.Statistics; import edu.kit.kastel.sdq.lissa.ratlr.configuration.EvaluationConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.configuration.GoldStandardConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.TraceLink; -import edu.kit.kastel.sdq.lissa.ratlr.utils.KeyGenerator; import picocli.CommandLine; diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Evaluation.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Evaluation.java index 6b2d2ab4..ee84934e 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Evaluation.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Evaluation.java @@ -14,8 +14,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager; import edu.kit.kastel.sdq.lissa.ratlr.artifactprovider.ArtifactProvider; -import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager; import edu.kit.kastel.sdq.lissa.ratlr.classifier.Classifier; import edu.kit.kastel.sdq.lissa.ratlr.configuration.EvaluationConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.configuration.EvaluationConfigurationBuilder; diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Main.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Main.java index 7f3d79ed..ce698958 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Main.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Main.java @@ -9,8 +9,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager; import edu.kit.kastel.sdq.lissa.ratlr.artifactprovider.ArtifactProvider; -import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager; import edu.kit.kastel.sdq.lissa.ratlr.classifier.Classifier; import edu.kit.kastel.sdq.lissa.ratlr.configuration.EvaluationConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.context.ContextStore; diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Optimization.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Optimization.java index a850ee3c..4a5df284 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Optimization.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/Optimization.java @@ -14,7 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import edu.kit.kastel.sdq.lissa.ratlr.cache.CacheManager; +import edu.kit.kastel.mcse.ardoco.llm.cache.CacheManager; import edu.kit.kastel.sdq.lissa.ratlr.configuration.OptimizerConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.TraceLink; import edu.kit.kastel.sdq.lissa.ratlr.promptoptimizer.PromptOptimizer; diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java deleted file mode 100644 index 2cee2c7e..00000000 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java +++ /dev/null @@ -1,146 +0,0 @@ -/* Licensed under MIT 2025-2026. */ -package edu.kit.kastel.sdq.lissa.ratlr.cache; - -import org.jspecify.annotations.Nullable; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Interface for cache implementations in the LiSSA framework. - * This interface defines the contract for caching mechanisms that store and retrieve - * values associated with cache keys. - * - * @param The type of cache key used in this cache - */ -public interface Cache { - /** - * Retrieves a value from the cache and deserializes it to the specified type. - * - * @param The type to deserialize the cached 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 - */ - @Nullable T get(String key, Class clazz); - - /** - * Retrieves a value from the cache and deserializes it to the specified type. - * DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING. - * - * @param The type to deserialize the cached 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 - * @deprecated This method exposes internal cache key handling and should not be used in general code. - */ - @Deprecated(forRemoval = false) - @Nullable T getViaInternalKey(K key, Class clazz); - - /** - * Stores a string value in the cache. - * - * @param key The cache key to store the value under - * @param value The string value to store - */ - void put(String key, String value); - - /** - * Stores a string value in the cache. - * - * @param The type of the value to store - * @param key 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. - */ - @Deprecated(forRemoval = false) - void putViaInternalKey(K key, T value); - - /** - * Stores an object value in the cache. - * The object will be serialized 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 - */ - void put(String key, T value); - - /** - * Flushes any pending changes to the cache storage. - * This method should be called to ensure all cached values are persisted. - */ - void flush(); - - /** - * Returns true if this map contains a mapping for the specified key. - * More formally, returns true if and only if this map contains a mapping for a key k such that Objects.equals(key, k). - * (There can be at most one such mapping.) - * - * @param key The cache key to check for existence - * @return true if this map contains a mapping for the specified key - */ - boolean containsKey(String key); - - /** - * Gets the cache parameters used to configure this cache. - * - * @return The cache parameters - */ - CacheParameter getCacheParameter(); - - /** - * Converts a JSON string to an object of the specified type. - * If the target type is String, the JSON string is returned as is. - * - * @param The type to convert to - * @param jsonData The JSON string to convert - * @param clazz The class of the target type - * @param mapper The ObjectMapper instance to use for deserialization - * @return The converted object, or null if jsonData is null - * @throws IllegalArgumentException If the JSON cannot be deserialized to the target type - */ - @SuppressWarnings("unchecked") - static @Nullable T convert(@Nullable String jsonData, Class clazz, ObjectMapper mapper) { - if (jsonData == null) { - return null; - } - try { - return mapper.readValue(jsonData, clazz); - } catch (JsonProcessingException e) { - // Backward compatibility for unserialized String values - if (clazz == String.class) { - return (T) jsonData; - } - throw new IllegalArgumentException("Could not deserialize object", e); - } - } - - /** - * Factory method to create a cache instance by type name. - * Supported types: - *
    - *
  • {@link CacheType#LOCAL} - file-based storage
  • - *
  • {@link CacheType#REDIS} - Redis-based storage
  • - *
  • {@link CacheType#REST_REDIS} - REST-based Redis storage
  • - *
- * - * @see CacheType - * - * @param The type of cache key - * @param type The cache type name (case-insensitive) - * @param cacheDir The directory for local cache storage - * @param parameters The cache parameters - * @param mapper The ObjectMapper for JSON operations - * @return A cache instance of the specified type - * @throws IllegalArgumentException If the type is not recognized or the cache cannot be created - */ - static Cache createByType( - CacheType type, CacheParameter 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: - *

    - *
  1. First checks the environment variable CACHE_REPLACEMENT_STRATEGY
  2. - *
  3. If not found, uses the default strategy ({@link #DEFAULT_REPLACEMENT_STRATEGY})
  4. - *
- * - * @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..6da29b17 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,108 @@ /* 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; - } - - /** - * Determines the number of threads to use based on the platform. - * OpenAI and Blablador platforms use 100 threads, while others use 1. - * - * @param configuration The module configuration - * @return The number of threads to use - */ - public static int threads(ModuleConfiguration configuration) { - return ChatLanguageModelPlatform.fromModuleConfiguration(configuration).getThreads(); + return delegate.temperature(); } /** - * Creates an Ollama chat model instance. - * The model is configured with authentication if credentials are provided. + * Returns the cache parameters that uniquely identify the model 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 + * @return The chat cache parameters */ - 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(); - }); + public ChatCacheParameter cacheParameters() { + return delegate.cacheParameters(); } /** - * Creates an OpenAI chat model instance. - * Requires OpenAI organization ID and API key to be set in environment variables. + * 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 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 platform(configuration).getThreads(); } - /** - * 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", platform.getDefaultModel()); + 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/Classifier.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java index 80ac0eea..69973e6c 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java @@ -16,12 +16,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; 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.elementstore.SourceElementStore; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.TargetElementStore; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * Abstract base class for trace link classifiers in the LiSSA framework. 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/PipelineClassifier.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java index 2c55b68e..00ccc97f 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java @@ -4,12 +4,12 @@ import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; 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.elementstore.SourceElementStore; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.TargetElementStore; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A classifier that processes elements through a pipeline of multiple classifier stages. 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..9ee53ebd 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; @@ -9,8 +9,8 @@ import org.jspecify.annotations.Nullable; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A store for elements and their embeddings in the LiSSA framework. diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java index 194c7c8d..88fd891f 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java @@ -5,8 +5,8 @@ import java.util.LinkedHashMap; import java.util.Map; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A utility class for operations related to ElementStores. 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..b85f14b0 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,12 +1,12 @@ -/* Licensed under MIT 2025. */ +/* Licensed under MIT 2025-2026. */ package edu.kit.kastel.sdq.lissa.ratlr.elementstore; import java.util.List; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.strategy.RetrievalStrategy; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A store for source elements and their embeddings in the LiSSA framework. 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..890a19ab 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,12 +1,12 @@ -/* Licensed under MIT 2025. */ +/* Licensed under MIT 2025-2026. */ package edu.kit.kastel.sdq.lissa.ratlr.elementstore; import java.util.List; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.strategy.RetrievalStrategy; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A store for target elements and their embeddings in the LiSSA framework. diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java index ce942702..97e596c6 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java @@ -4,9 +4,9 @@ import java.util.ArrayList; import java.util.List; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A retrieval strategy that computes the cosine similarity between a query vector and diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java index dd11629b..e8cde519 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java @@ -6,9 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * Interface defining the contract for retrieval strategies used to find similar elements based on their vector representations. 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: - *
    - *
  1. First, generates a unique cache key based on the element's content
  2. - *
  3. Checks if a cached embedding exists for this key
  4. - *
  5. If cached, returns the existing embedding immediately
  6. - *
  7. 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}
    • - *
    - *
  8. - *
- * - * 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..c0b0e7f4 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,86 @@ -/* 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(); + case "mock" -> EmbeddingConfiguration.of(EmbeddingPlatform.MOCK); 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: - *
    - *
  1. Testing: Provides a simple way to test the embedding pipeline without - * incurring the computational cost of real embedding generation
  2. - *
  3. 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
  4. - *
- * - * 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..b4d7b70b 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; @@ -21,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.classifier.ClassificationTask; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.SourceElementStore; @@ -28,7 +29,6 @@ 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.promptoptimizer.promptselector.Selector; -import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * This class implements an automatic prompt optimizer based on the approach by Pryzant et al. (2023). diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java index bf1fffb1..11c38a10 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java @@ -10,12 +10,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.classifier.ClassificationTask; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.SourceElementStore; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.TargetElementStore; 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.utils.Pair; public final class PromptOptimizationUtils { 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: - *
    - *
  1. Values from the .env file (if it exists)
  2. - *
  3. Values from system environment variables
  4. - *
- * - * 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: - *
    - *
  1. First checks the .env file for the variable
  2. - *
  3. If not found, falls back to system environment variables
  4. - *
  5. Returns null if the variable is not found in either location
  6. - *
- * - * @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: - *
    - *
  1. Attempts to retrieve the variable using {@link #getenv(String)}
  2. - *
  3. Throws an IllegalStateException if environment variable would be null
  4. - *
- * - * @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: - *
    - *
  1. Checks if a .env file exists in the project root
  2. - *
  3. If found, loads and returns the configuration
  4. - *
  5. If not found, logs a message and returns null
  6. - *
- * - * 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: - *
    - *
  1. Checks if a .env file exists at the given path
  2. - *
  3. If found, loads and sets the new configuration
  4. - *
  5. If not found, logs a warning and retains the existing configuration
  6. - *
- * - * 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: - *
    - *
  1. Normalizes line endings in the input string
  2. - *
  3. Converts the string into a UUID format
  4. - *
- * - * @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/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java deleted file mode 100644 index 44720250..00000000 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java +++ /dev/null @@ -1,12 +0,0 @@ -/* Licensed under MIT 2025-2026. */ -package edu.kit.kastel.sdq.lissa.ratlr.utils; - -/** - * A utility record class representing an immutable pair of values. - * - * @param The type of the first value in the pair - * @param The type of the second value in the pair - * @param first The first value of the pair - * @param second The second value of the pair - */ -public record Pair(F first, S second) {} 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..ce3da0cd 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. Rules that used to + * enforce invariants of the LLM/cache utilities now live in the {@code llm-access} library, which owns + * those classes. */ @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/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/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 { From 39ab713b44589221489e7c592e85db776b880f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Sun, 2 Aug 2026 01:44:49 +0200 Subject: [PATCH 2/6] Docs --- docs/architecture.md | 10 ++----- docs/caching.md | 61 ++++++++++++------------------------------- docs/configuration.md | 3 +-- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index d9e0191f..c68080ce 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -24,14 +24,8 @@ The project follows a modular architecture with the following main components: - [`SummarizePreprocessor`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/SummarizePreprocessor.java): Uses LLMs to generate concise summaries of artifacts while preserving key information, with configurable templates for different artifact types. - [`SentencePreprocessor`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/SentencePreprocessor.java): Splits text documents into individual sentences while maintaining the original document as a parent element. 3. **Embedding Creators** (`embeddingcreator` package) - - [`EmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java): Base class for creating embeddings - - Implementations: - - [`OpenAiEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenAiEmbeddingCreator.java): Uses OpenAI's embedding models to create vector representations of text, supporting various models like text-embedding-3-large. - - [`OllamaEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OllamaEmbeddingCreator.java): Integrates with Ollama's local embedding models, providing an alternative to cloud-based solutions. - - [`OpenWebUiEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenWebUiEmbeddingCreator.java): Integrates with Open WebUI servers for embedding generation, supporting local deployment with OpenAI-compatible APIs. - - [`OnnxEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OnnxEmbeddingCreator.java): Uses ONNX models for local embedding generation, offering high performance and offline capabilities. - - [`MockEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/MockEmbeddingCreator.java): Provides zero vectors for testing purposes, useful for development and testing scenarios. - - All extend [`CachedEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/CachedEmbeddingCreator.java) for caching support, improving performance by storing and reusing embeddings. + - [`EmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java): LiSSA-side adapter that maps LiSSA `Element`s and configuration to the framework-neutral embedding creators of the [`io.github.ardoco:llm-access`](https://github.com/ardoco/llm-access) library. + - The library provides the actual implementations (OpenAI, Ollama, ONNX, Open WebUI, and a mock), all with transparent caching and token-length handling. The `openai` creator uses OpenAI embedding models such as `text-embedding-3-large`; `ollama`/`openwebui` integrate with local/OpenAI-compatible endpoints; `onnx` runs models locally for offline use. 4. **Element Stores** (`elementstore` package) - [`ElementStore`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStore.java): Manages storage and retrieval of processed elements with their embeddings, supporting similarity-based search and hierarchical relationships. - **Retrieval Strategies** (`elementstore/strategy` package): diff --git a/docs/caching.md b/docs/caching.md index d0098223..6d66f5b1 100644 --- a/docs/caching.md +++ b/docs/caching.md @@ -2,50 +2,21 @@ ## Overview -LiSSA implements a sophisticated caching system to improve performance and ensure reproducibility of results. The caching system consists of the following components: - -1. **Cache Interface** (`cache` package) - - [`Cache`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java): Core generic interface defining cache operations, parameterized by cache key type - - [`CacheKey`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheKey.java): Base interface for cache keys with JSON serialization support and local key generation - - [`CacheParameter`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheParameter.java): Interface defining cache configuration and key creation logic - - **Specialized Cache Keys**: - - [`ClassifierCacheKey`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheKey.java): Cache key for classifier operations (model name, seed, temperature, mode, content) - - [`EmbeddingCacheKey`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheKey.java): Cache key for embedding operations (model name, content) - - **Cache Parameters**: - - [`ClassifierCacheParameter`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/classifier/ClassifierCacheParameter.java): Configuration for classifier caches (model name, seed, temperature) - - [`EmbeddingCacheParameter`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/embedding/EmbeddingCacheParameter.java): Configuration for embedding caches (model name) -2. **Cache Implementations** - - [`Hierarchical Cache`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/HierarchicalCache.java): Two cache levels with a synchronization mechanism - - Changes are applied to both levels - - Reads use a Conflict Resolution Strategy to ensure consistent results - - If a cache entry is missing in one level during a read, it is also written to the other level - - [`LocalCache`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/LocalCache.java): File-based cache implementation that stores data in JSON format - - Implements dirty tracking to optimize writes - - Automatically saves changes on shutdown - - Supports atomic writes using temporary files - - [`RedisCache`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java): Redis-based cache implementation - - Uses Redis for high-performance caching - - Supports both string and object serialization - - [`RestRedisCache`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java): REST-based Redis cache implementation - - Uses REST API to interact with Redis server - - Provides an alternative to direct Redis connections, useful for shared caches - - Configuration through environment variables, see [Usage Instructions](#usage-instructions) -3. **Cache Management** - - [`CacheManager`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/CacheManager.java): Central manager for cache instances - - Manages cache directory configuration - - Provides singleton access to cache instances - - Handles cache creation and retrieval based on origin and cache parameters - - Ensures cache uniqueness by validating parameters -4. **Caching Usage** - The caching system is used in several key components: - - **Embedding Creators**: Caches vector embeddings to avoid recalculating them - - Uses `EmbeddingCacheParameter` to identify unique embedding configurations - - Cache keys are automatically generated based on content using the model name - - **Classifiers**: Caches LLM responses for classification tasks - - Uses `ClassifierCacheParameter` to identify unique classifier configurations - - Cache keys include model name, seed, temperature, and content - - **Preprocessors**: Caches preprocessing results for text summarization and other operations - - Uses `ClassifierCacheParameter` for LLM-based preprocessing +LiSSA relies on the caching subsystem provided by the [`io.github.ardoco:llm-access`](https://github.com/ardoco/llm-access) library to improve performance and ensure reproducibility of results. That library owns the cache abstraction and its implementations, so LiSSA no longer ships its own copies: + +1. **Cache abstraction**: `Cache`, `CacheKey`, and `CacheParameter`, plus typed keys/parameters for chat (`ChatCacheKey` / `ChatCacheParameter`) and embedding (`EmbeddingCacheKey` / `EmbeddingCacheParameter`) operations. +2. **Cache implementations**: a hierarchical (two-level) cache with a conflict-resolution strategy, a file-based `LocalCache` (JSON, atomic writes, dirty tracking), a `RedisCache`, and a REST-based `RestRedisCache`. +3. **Cache management**: a `CacheManager` that configures the cache directory and provides cache instances keyed by origin and parameters. + +See the [llm-access documentation](https://github.com/ardoco/llm-access) for the cache internals. The rest of this page describes how LiSSA configures and uses the cache (the configuration and environment variables are unchanged). + +### Caching Usage + +The caching system is used in several key components: + +- **Embedding Creators**: cache vector embeddings to avoid recalculating them (keyed by `EmbeddingCacheParameter`: model name). +- **Classifiers**: cache LLM responses for classification tasks (keyed by `ChatCacheParameter`: model name, seed, temperature, content). +- **Preprocessors**: cache results of LLM-based preprocessing (keyed by `ChatCacheParameter`). ## Key Concepts @@ -58,7 +29,7 @@ Cache keys uniquely identify cached items and consist of two parts: ### Cache Parameters Cache parameters define the configuration that makes a cache unique: -- **ClassifierCacheParameter**: Model name, seed, and temperature for reproducible LLM results +- **ChatCacheParameter**: Model name, seed, and temperature for reproducible LLM results - **EmbeddingCacheParameter**: Model name only (embeddings are deterministic) Parameters are used to: diff --git a/docs/configuration.md b/docs/configuration.md index 2df08001..586b3e90 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,8 +13,7 @@ Configuration options in LiSSA are defined in the code through several mechanism 1. **Component Classes**: Each component (e.g., `ArtifactProvider`, `Preprocessor`, `Classifier`) has a corresponding class that defines its configuration options. For example: - [`TextArtifactProvider`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/artifactprovider/TextArtifactProvider.java) defines options for text-based artifact loading - [`CodeTreePreprocessor`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/preprocessor/CodeTreePreprocessor.java) defines options for code tree processing - - [`OpenAiEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenAiEmbeddingCreator.java) defines options for OpenAI embedding generation - - [`OpenWebUiEmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/OpenWebUiEmbeddingCreator.java) defines options for Open WebUI embedding generation + - the [`EmbeddingCreator`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreator.java) adapter maps the `embedding_creator` configuration (e.g. `openai`, `ollama`, `onnx`, `openwebui`, `mock`) to the embedding creators provided by the [`io.github.ardoco:llm-access`](https://github.com/ardoco/llm-access) library 2. **Configuration Classes**: The [`Configuration`](../src/main/java/edu/kit/kastel/sdq/lissa/ratlr/configuration/Configuration.java) class serves as the central configuration container, defining the structure of the configuration file. 3. **Example Configurations**: You can find example configurations in the `example-configs` directory, which demonstrate different configuration setups for various use cases. 4. **Configuration Template**: The `config-template.json` file provides a template with all available configuration options and their default values. From f27af606e572ae490222472e9b3a3c9e13900e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Sun, 2 Aug 2026 14:47:32 +0200 Subject: [PATCH 3/6] Update to current library --- .../lissa/ratlr/classifier/ChatLanguageModelProvider.java | 8 ++++++-- .../lissa/ratlr/embeddingcreator/EmbeddingCreator.java | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) 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 6da29b17..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 @@ -82,12 +82,16 @@ public ChatCacheParameter cacheParameters() { * @return The number of threads to use */ public static int threads(ModuleConfiguration configuration) { - return platform(configuration).getThreads(); + return switch (platform(configuration)) { + case OPENAI, BLABLADOR, DEEPSEEK -> 100; + case OPENWEBUI -> 10; + case OLLAMA -> 1; + }; } private static LlmConfiguration toLlmConfiguration(ModuleConfiguration configuration) { ChatModelPlatform platform = platform(configuration); - String model = configuration.argumentAsString("model", platform.getDefaultModel()); + 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) 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 c0b0e7f4..dca04512 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 @@ -80,7 +80,9 @@ private static EmbeddingConfiguration toEmbeddingConfiguration(ModuleConfigurati EmbeddingConfiguration.builder(EmbeddingPlatform.OPENWEBUI) .modelName(configuration.argumentAsString("model", "nomic-embed-text:v1.5")) .build(); - case "mock" -> EmbeddingConfiguration.of(EmbeddingPlatform.MOCK); + // 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()); }; } From cba0462899f8b395ae009df14e0b163115481b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Sun, 2 Aug 2026 17:19:30 +0200 Subject: [PATCH 4/6] Add more tests --- .../embeddingcreator/EmbeddingCreator.java | 3 +- .../lissa/ratlr/e2e/MockPipelineE2ETest.java | 42 ++++++++++++++ .../EmbeddingCreatorTest.java | 38 +++++++++++++ src/test/resources/warc/config-mock.json | 57 +++++++++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/e2e/MockPipelineE2ETest.java create mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/embeddingcreator/EmbeddingCreatorTest.java create mode 100644 src/test/resources/warc/config-mock.json 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 dca04512..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 @@ -82,7 +82,8 @@ private static EmbeddingConfiguration toEmbeddingConfiguration(ModuleConfigurati .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(); + case "mock" -> + EmbeddingConfiguration.builder(EmbeddingPlatform.MOCK).build(); default -> throw new IllegalStateException("Unexpected value: " + configuration.name()); }; } 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/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" : {} + } +} From add6c69e4d2c7b4a06bfedd3b43135b360a59f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Wed, 5 Aug 2026 14:58:47 +0200 Subject: [PATCH 5/6] Update to current LLM-access lib --- .../sdq/lissa/ratlr/classifier/Classifier.java | 2 +- .../lissa/ratlr/classifier/PipelineClassifier.java | 2 +- .../sdq/lissa/ratlr/elementstore/ElementStore.java | 2 +- .../ratlr/elementstore/ElementStoreOperations.java | 2 +- .../lissa/ratlr/elementstore/SourceElementStore.java | 2 +- .../lissa/ratlr/elementstore/TargetElementStore.java | 2 +- .../elementstore/strategy/CosineSimilarity.java | 2 +- .../elementstore/strategy/RetrievalStrategy.java | 2 +- .../ratlr/promptoptimizer/ProTeGiOptimizer.java | 2 +- .../promptoptimizer/PromptOptimizationUtils.java | 2 +- .../edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java | 12 ++++++++++++ 11 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java index 69973e6c..80ac0eea 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/Classifier.java @@ -16,12 +16,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; 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.elementstore.SourceElementStore; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.TargetElementStore; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * Abstract base class for trace link classifiers in the LiSSA framework. diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java index 00ccc97f..2c55b68e 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/classifier/PipelineClassifier.java @@ -4,12 +4,12 @@ import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; 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.elementstore.SourceElementStore; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.TargetElementStore; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A classifier that processes elements through a pipeline of multiple classifier stages. 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 9ee53ebd..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 @@ -9,8 +9,8 @@ import org.jspecify.annotations.Nullable; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A store for elements and their embeddings in the LiSSA framework. diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java index 88fd891f..194c7c8d 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/ElementStoreOperations.java @@ -5,8 +5,8 @@ import java.util.LinkedHashMap; import java.util.Map; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A utility class for operations related to ElementStores. 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 b85f14b0..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 @@ -3,10 +3,10 @@ import java.util.List; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.strategy.RetrievalStrategy; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A store for source elements and their embeddings in the LiSSA framework. 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 890a19ab..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 @@ -3,10 +3,10 @@ import java.util.List; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.strategy.RetrievalStrategy; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A store for target elements and their embeddings in the LiSSA framework. diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java index 97e596c6..ce942702 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/CosineSimilarity.java @@ -4,9 +4,9 @@ import java.util.ArrayList; import java.util.List; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * A retrieval strategy that computes the cosine similarity between a query vector and diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java index e8cde519..dd11629b 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/elementstore/strategy/RetrievalStrategy.java @@ -6,9 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.knowledge.Element; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * Interface defining the contract for retrieval strategies used to find similar elements based on their vector representations. 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 b4d7b70b..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 @@ -21,7 +21,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.classifier.ClassificationTask; import edu.kit.kastel.sdq.lissa.ratlr.configuration.ModuleConfiguration; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.SourceElementStore; @@ -29,6 +28,7 @@ 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.promptoptimizer.promptselector.Selector; +import edu.kit.kastel.sdq.lissa.ratlr.utils.Pair; /** * This class implements an automatic prompt optimizer based on the approach by Pryzant et al. (2023). diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java index 11c38a10..bf1fffb1 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/promptoptimizer/PromptOptimizationUtils.java @@ -10,12 +10,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import edu.kit.kastel.mcse.ardoco.llm.util.Pair; import edu.kit.kastel.sdq.lissa.ratlr.classifier.ClassificationTask; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.SourceElementStore; import edu.kit.kastel.sdq.lissa.ratlr.elementstore.TargetElementStore; 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.utils.Pair; public final class PromptOptimizationUtils { diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java new file mode 100644 index 00000000..44720250 --- /dev/null +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/utils/Pair.java @@ -0,0 +1,12 @@ +/* Licensed under MIT 2025-2026. */ +package edu.kit.kastel.sdq.lissa.ratlr.utils; + +/** + * A utility record class representing an immutable pair of values. + * + * @param The type of the first value in the pair + * @param The type of the second value in the pair + * @param first The first value of the pair + * @param second The second value of the pair + */ +public record Pair(F first, S second) {} From dd7ef8e195f89aee25e3d6b7a136791da0dfe2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Wed, 5 Aug 2026 15:18:14 +0200 Subject: [PATCH 6/6] Fix review comments --- example-configs/transitive/d2m.json | 1 + example-configs/transitive/m2c.json | 1 + .../sdq/lissa/ratlr/ArchitectureTest.java | 6 +- .../ratlr/LlmAccessArchitectureTest.java | 251 ++++++++++++++++++ 4 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/LlmAccessArchitectureTest.java diff --git a/example-configs/transitive/d2m.json b/example-configs/transitive/d2m.json index e743e7ee..395bd6be 100644 --- a/example-configs/transitive/d2m.json +++ b/example-configs/transitive/d2m.json @@ -45,6 +45,7 @@ "classifier" : { "name" : "reasoning_openai", "args" : { + "model" : "gpt-4o-mini" } }, "result_aggregator" : { diff --git a/example-configs/transitive/m2c.json b/example-configs/transitive/m2c.json index 631a9a2b..fe3fc232 100644 --- a/example-configs/transitive/m2c.json +++ b/example-configs/transitive/m2c.json @@ -47,6 +47,7 @@ "classifier" : { "name" : "reasoning_openai", "args" : { + "model" : "gpt-4o-mini" } }, "result_aggregator" : { 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 ce3da0cd..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 @@ -28,9 +28,9 @@ /** * Architecture tests for the LiSSA framework using ArchUnit. *

- * These rules help maintain code quality, consistency, and architectural integrity. Rules that used to - * enforce invariants of the LLM/cache utilities now live in the {@code llm-access} library, which owns - * those classes. + * 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 { 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())); + } + } + } + }); +}