V1.1 Änderungen
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config.provider;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Supported AI provider families for the PDF renaming process.
|
||||
* <p>
|
||||
* Each constant represents a distinct API protocol family. Exactly one provider family
|
||||
* is active per application run, selected via the {@code ai.provider.active} configuration property.
|
||||
* <p>
|
||||
* The {@link #getIdentifier()} method returns the string that must appear as the value of
|
||||
* {@code ai.provider.active} to activate the corresponding provider family.
|
||||
* Use {@link #fromIdentifier(String)} to resolve a configuration string to the enum constant.
|
||||
*/
|
||||
public enum AiProviderFamily {
|
||||
|
||||
/** OpenAI-compatible Chat Completions API – usable with OpenAI itself and compatible third-party endpoints. */
|
||||
OPENAI_COMPATIBLE("openai-compatible"),
|
||||
|
||||
/** Native Anthropic Messages API for Claude models. */
|
||||
CLAUDE("claude");
|
||||
|
||||
private final String identifier;
|
||||
|
||||
AiProviderFamily(String identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration identifier string for this provider family.
|
||||
* <p>
|
||||
* This value corresponds to valid values of the {@code ai.provider.active} property.
|
||||
*
|
||||
* @return the configuration identifier, never {@code null}
|
||||
*/
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a provider family from its configuration identifier string.
|
||||
* <p>
|
||||
* The comparison is case-sensitive and matches the exact identifier strings
|
||||
* defined by each constant (e.g., {@code "openai-compatible"}, {@code "claude"}).
|
||||
*
|
||||
* @param identifier the identifier as it appears in the {@code ai.provider.active} property;
|
||||
* {@code null} returns an empty Optional
|
||||
* @return the matching provider family, or {@link Optional#empty()} if not recognized
|
||||
*/
|
||||
public static Optional<AiProviderFamily> fromIdentifier(String identifier) {
|
||||
if (identifier == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Arrays.stream(values())
|
||||
.filter(f -> f.identifier.equals(identifier))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config.provider;
|
||||
|
||||
/**
|
||||
* Immutable multi-provider configuration model.
|
||||
* <p>
|
||||
* Represents the resolved configuration for both supported AI provider families
|
||||
* together with the selection of the one provider family that is active for this
|
||||
* application run.
|
||||
*
|
||||
* <h2>Invariants</h2>
|
||||
* <ul>
|
||||
* <li>Exactly one provider family is active per run.</li>
|
||||
* <li>Required fields are enforced only for the active provider; the inactive
|
||||
* provider's configuration may be incomplete.</li>
|
||||
* <li>Validation of these invariants is performed by the corresponding validator
|
||||
* in the adapter layer, not by this record itself.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param activeProviderFamily the selected provider family for this run; {@code null}
|
||||
* indicates that {@code ai.provider.active} was absent or
|
||||
* held an unrecognised value – the validator will reject this
|
||||
* @param openAiCompatibleConfig configuration for the OpenAI-compatible provider family
|
||||
* @param claudeConfig configuration for the Anthropic Claude provider family
|
||||
*/
|
||||
public record MultiProviderConfiguration(
|
||||
AiProviderFamily activeProviderFamily,
|
||||
ProviderConfiguration openAiCompatibleConfig,
|
||||
ProviderConfiguration claudeConfig) {
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderConfiguration} for the currently active provider family.
|
||||
*
|
||||
* @return the active provider's configuration, never {@code null} when
|
||||
* {@link #activeProviderFamily()} is not {@code null}
|
||||
* @throws NullPointerException if {@code activeProviderFamily} is {@code null}
|
||||
*/
|
||||
public ProviderConfiguration activeProviderConfiguration() {
|
||||
return switch (activeProviderFamily) {
|
||||
case OPENAI_COMPATIBLE -> openAiCompatibleConfig;
|
||||
case CLAUDE -> claudeConfig;
|
||||
};
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config.provider;
|
||||
|
||||
/**
|
||||
* Immutable configuration for a single AI provider family.
|
||||
* <p>
|
||||
* Holds all parameters needed to connect to and authenticate with one AI provider endpoint.
|
||||
* Instances are created by the configuration parser in the adapter layer; validation
|
||||
* of required fields is performed by the corresponding validator.
|
||||
*
|
||||
* <h2>Field semantics</h2>
|
||||
* <ul>
|
||||
* <li>{@code model} – the AI model name; required for the active provider, may be {@code null}
|
||||
* for the inactive provider.</li>
|
||||
* <li>{@code timeoutSeconds} – HTTP connection/read timeout in seconds; must be positive for
|
||||
* the active provider. {@code 0} indicates the value was not configured.</li>
|
||||
* <li>{@code baseUrl} – the base URL of the API endpoint. For the Anthropic Claude family a
|
||||
* default of {@code https://api.anthropic.com} is applied by the parser when the property
|
||||
* is absent; for the OpenAI-compatible family it is required and may not be {@code null}.</li>
|
||||
* <li>{@code apiKey} – the resolved API key after environment-variable precedence has been
|
||||
* applied; may be blank for the inactive provider, must not be blank for the active provider.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param model the AI model name; {@code null} when not configured
|
||||
* @param timeoutSeconds HTTP timeout in seconds; {@code 0} when not configured
|
||||
* @param baseUrl the base URL of the API endpoint; {@code null} when not configured
|
||||
* (only applicable to providers without a built-in default)
|
||||
* @param apiKey the resolved API key; blank when not configured
|
||||
*/
|
||||
public record ProviderConfiguration(
|
||||
String model,
|
||||
int timeoutSeconds,
|
||||
String baseUrl,
|
||||
String apiKey) {
|
||||
}
|
||||
+11
-6
@@ -1,16 +1,24 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config.startup;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
|
||||
|
||||
/**
|
||||
* Typed immutable configuration model for PDF Umbenenner startup parameters.
|
||||
* <p>
|
||||
* Contains all technical infrastructure and runtime configuration parameters
|
||||
* loaded and validated at bootstrap time. This is a complete configuration model
|
||||
* for the entire application startup, including paths, API settings, persistence,
|
||||
* for the entire application startup, including paths, AI provider selection, persistence,
|
||||
* and operational parameters.
|
||||
*
|
||||
* <h2>AI provider configuration</h2>
|
||||
* <p>
|
||||
* The {@link MultiProviderConfiguration} encapsulates the active provider selection
|
||||
* together with the per-provider connection parameters for all supported provider families.
|
||||
* Exactly one provider family is active per run; the selection is driven by the
|
||||
* {@code ai.provider.active} configuration property.
|
||||
*
|
||||
* <h2>AI content sensitivity ({@code log.ai.sensitive})</h2>
|
||||
* <p>
|
||||
* The boolean property {@code log.ai.sensitive} controls whether sensitive AI-generated
|
||||
@@ -25,9 +33,7 @@ public record StartConfiguration(
|
||||
Path sourceFolder,
|
||||
Path targetFolder,
|
||||
Path sqliteFile,
|
||||
URI apiBaseUrl,
|
||||
String apiModel,
|
||||
int apiTimeoutSeconds,
|
||||
MultiProviderConfiguration multiProviderConfiguration,
|
||||
int maxRetriesTransient,
|
||||
int maxPages,
|
||||
int maxTextCharacters,
|
||||
@@ -35,7 +41,6 @@ public record StartConfiguration(
|
||||
Path runtimeLockFile,
|
||||
Path logDirectory,
|
||||
String logLevel,
|
||||
String apiKey,
|
||||
|
||||
/**
|
||||
* Whether sensitive AI content (raw response, reasoning) may be written to log files.
|
||||
|
||||
+9
-2
@@ -42,6 +42,10 @@ import java.util.Objects;
|
||||
* successful or skip attempts.</li>
|
||||
* <li>{@link #retryable()} — {@code true} if the failure is considered retryable in a
|
||||
* later run; {@code false} for final failures, successes, and skip attempts.</li>
|
||||
* <li>{@link #aiProvider()} — opaque identifier of the AI provider that was active
|
||||
* during this attempt (e.g. {@code "openai-compatible"} or {@code "claude"});
|
||||
* {@code null} for attempts that did not involve an AI call (skip, pre-check
|
||||
* failure) or for historical attempts recorded before this field was introduced.</li>
|
||||
* <li>{@link #modelName()} — the AI model name used in this attempt; {@code null} if
|
||||
* no AI call was made (e.g. pre-check failures or skip attempts).</li>
|
||||
* <li>{@link #promptIdentifier()} — stable identifier of the prompt template used;
|
||||
@@ -74,6 +78,7 @@ import java.util.Objects;
|
||||
* @param failureClass failure classification, or {@code null} for non-failure statuses
|
||||
* @param failureMessage failure description, or {@code null} for non-failure statuses
|
||||
* @param retryable whether this failure should be retried in a later run
|
||||
* @param aiProvider opaque AI provider identifier for this attempt, or {@code null}
|
||||
* @param modelName AI model name, or {@code null} if no AI call was made
|
||||
* @param promptIdentifier prompt identifier, or {@code null} if no AI call was made
|
||||
* @param processedPageCount number of PDF pages processed, or {@code null}
|
||||
@@ -97,6 +102,7 @@ public record ProcessingAttempt(
|
||||
String failureMessage,
|
||||
boolean retryable,
|
||||
// AI traceability fields (null for non-AI attempts)
|
||||
String aiProvider,
|
||||
String modelName,
|
||||
String promptIdentifier,
|
||||
Integer processedPageCount,
|
||||
@@ -131,7 +137,8 @@ public record ProcessingAttempt(
|
||||
* Creates a {@link ProcessingAttempt} with no AI traceability fields set.
|
||||
* <p>
|
||||
* Convenience factory for pre-check failures, skip events, and any attempt
|
||||
* that does not involve an AI call.
|
||||
* that does not involve an AI call. The {@link #aiProvider()} field is set
|
||||
* to {@code null}.
|
||||
*
|
||||
* @param fingerprint document identity; must not be null
|
||||
* @param runId batch run identifier; must not be null
|
||||
@@ -157,6 +164,6 @@ public record ProcessingAttempt(
|
||||
return new ProcessingAttempt(
|
||||
fingerprint, runId, attemptNumber, startedAt, endedAt,
|
||||
status, failureClass, failureMessage, retryable,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
null, null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-5
@@ -154,15 +154,22 @@ public class DocumentProcessingCoordinator {
|
||||
private final TargetFileCopyPort targetFileCopyPort;
|
||||
private final ProcessingLogger logger;
|
||||
private final int maxRetriesTransient;
|
||||
private final String activeProviderIdentifier;
|
||||
|
||||
/**
|
||||
* Creates the document processing coordinator with all required ports, logger, and
|
||||
* the transient retry limit.
|
||||
* Creates the document processing coordinator with all required ports, logger,
|
||||
* the transient retry limit, and the active AI provider identifier.
|
||||
* <p>
|
||||
* {@code maxRetriesTransient} is the maximum number of historised transient error attempts
|
||||
* per fingerprint before the document is finalised to
|
||||
* {@link ProcessingStatus#FAILED_FINAL}. The attempt that causes the counter to
|
||||
* reach this value finalises the document. Must be >= 1.
|
||||
* <p>
|
||||
* {@code activeProviderIdentifier} is the opaque string identifier of the AI provider
|
||||
* that is active for this run (e.g. {@code "openai-compatible"} or {@code "claude"}).
|
||||
* It is written to the attempt history for every attempt that involves an AI call,
|
||||
* enabling provider-level traceability per attempt without introducing
|
||||
* provider-specific logic in the application layer.
|
||||
*
|
||||
* @param documentRecordRepository port for reading and writing the document master record;
|
||||
* must not be null
|
||||
@@ -176,8 +183,11 @@ public class DocumentProcessingCoordinator {
|
||||
* @param logger for processing-related logging; must not be null
|
||||
* @param maxRetriesTransient maximum number of historised transient error attempts
|
||||
* before finalisation; must be >= 1
|
||||
* @param activeProviderIdentifier opaque identifier of the active AI provider for this run;
|
||||
* must not be null or blank
|
||||
* @throws NullPointerException if any object parameter is null
|
||||
* @throws IllegalArgumentException if {@code maxRetriesTransient} is less than 1
|
||||
* @throws IllegalArgumentException if {@code maxRetriesTransient} is less than 1, or
|
||||
* if {@code activeProviderIdentifier} is blank
|
||||
*/
|
||||
public DocumentProcessingCoordinator(
|
||||
DocumentRecordRepository documentRecordRepository,
|
||||
@@ -186,11 +196,16 @@ public class DocumentProcessingCoordinator {
|
||||
TargetFolderPort targetFolderPort,
|
||||
TargetFileCopyPort targetFileCopyPort,
|
||||
ProcessingLogger logger,
|
||||
int maxRetriesTransient) {
|
||||
int maxRetriesTransient,
|
||||
String activeProviderIdentifier) {
|
||||
if (maxRetriesTransient < 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"maxRetriesTransient must be >= 1, got: " + maxRetriesTransient);
|
||||
}
|
||||
Objects.requireNonNull(activeProviderIdentifier, "activeProviderIdentifier must not be null");
|
||||
if (activeProviderIdentifier.isBlank()) {
|
||||
throw new IllegalArgumentException("activeProviderIdentifier must not be blank");
|
||||
}
|
||||
this.documentRecordRepository =
|
||||
Objects.requireNonNull(documentRecordRepository, "documentRecordRepository must not be null");
|
||||
this.processingAttemptRepository =
|
||||
@@ -203,6 +218,7 @@ public class DocumentProcessingCoordinator {
|
||||
Objects.requireNonNull(targetFileCopyPort, "targetFileCopyPort must not be null");
|
||||
this.logger = Objects.requireNonNull(logger, "logger must not be null");
|
||||
this.maxRetriesTransient = maxRetriesTransient;
|
||||
this.activeProviderIdentifier = activeProviderIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +519,7 @@ public class DocumentProcessingCoordinator {
|
||||
ProcessingAttempt successAttempt = new ProcessingAttempt(
|
||||
fingerprint, context.runId(), attemptNumber, attemptStart, now,
|
||||
ProcessingStatus.SUCCESS, null, null, false,
|
||||
null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null,
|
||||
resolvedFilename);
|
||||
|
||||
DocumentRecord successRecord = buildSuccessRecord(
|
||||
@@ -951,6 +967,7 @@ public class DocumentProcessingCoordinator {
|
||||
yield new ProcessingAttempt(
|
||||
fingerprint, context.runId(), attemptNumber, startedAt, endedAt,
|
||||
outcome.overallStatus(), failureClass, failureMessage, outcome.retryable(),
|
||||
activeProviderIdentifier,
|
||||
ctx.modelName(), ctx.promptIdentifier(),
|
||||
ctx.processedPageCount(), ctx.sentCharacterCount(),
|
||||
ctx.aiRawResponse(),
|
||||
@@ -964,6 +981,7 @@ public class DocumentProcessingCoordinator {
|
||||
yield new ProcessingAttempt(
|
||||
fingerprint, context.runId(), attemptNumber, startedAt, endedAt,
|
||||
outcome.overallStatus(), failureClass, failureMessage, outcome.retryable(),
|
||||
activeProviderIdentifier,
|
||||
ctx.modelName(), ctx.promptIdentifier(),
|
||||
ctx.processedPageCount(), ctx.sentCharacterCount(),
|
||||
ctx.aiRawResponse(),
|
||||
@@ -976,6 +994,7 @@ public class DocumentProcessingCoordinator {
|
||||
yield new ProcessingAttempt(
|
||||
fingerprint, context.runId(), attemptNumber, startedAt, endedAt,
|
||||
outcome.overallStatus(), failureClass, failureMessage, outcome.retryable(),
|
||||
activeProviderIdentifier,
|
||||
ctx.modelName(), ctx.promptIdentifier(),
|
||||
ctx.processedPageCount(), ctx.sentCharacterCount(),
|
||||
ctx.aiRawResponse(),
|
||||
|
||||
Reference in New Issue
Block a user