1
0

M5 AP-002 JSON-Only-Erwartung in KI-Anfrage ergänzt und Tests geschärft

This commit is contained in:
2026-04-07 00:09:25 +02:00
parent cd5b6253df
commit 9ea6c3aaa5
2 changed files with 130 additions and 18 deletions

View File

@@ -15,16 +15,25 @@ import de.gecheckt.pdf.umbenenner.domain.model.PromptIdentifier;
* <li>The loaded prompt content (from the external prompt file)</li>
* <li>The stable prompt identifier (derived from the prompt source)</li>
* <li>The extracted document text (already limited to max characters if needed)</li>
* <li>An explicit JSON-only response format specification</li>
* <li>The exact character count that was sent (for traceability)</li>
* </ul>
* <p>
* <strong>Order and structure:</strong> The composition follows a fixed, documented order
* to ensure that another implementation would not need to guess how the prompt and document
* are combined. The prompt is presented first, followed by the document text, with clear
* structural markers to distinguish between them.
* <strong>Composition order:</strong> The request follows a fixed, documented order:
* <ol>
* <li>Prompt content (instruction)</li>
* <li>Prompt identifier marker</li>
* <li>Document text marker</li>
* <li>Document text content</li>
* <li>Response format expectation (JSON-only specification)</li>
* </ol>
* <p>
* <strong>JSON-only response expectation:</strong> The request is constructed with the
* explicit expectation that the AI will respond with a JSON object containing:
* This fixed order ensures reproducibility and allows another implementation to
* understand the request structure without guessing.
* <p>
* <strong>JSON-only response expectation:</strong> The actual composed request includes
* an explicit specification that the AI must respond with a JSON object containing
* exactly these fields:
* <ul>
* <li>{@code title} — mandatory, max 20 characters (base title)</li>
* <li>{@code reasoning} — mandatory, the AI's explanation</li>
@@ -38,22 +47,27 @@ public class AiRequestComposer {
/**
* Composes a deterministic AI request representation.
* <p>
* The composition order is fixed:
* The composition order is fixed and documented to ensure determinism and
* to make the structure obvious to any reader:
* <ol>
* <li>Prompt content</li>
* <li>Separator: newline</li>
* <li>Prompt identifier (for reference/traceability)</li>
* <li>Separator: newline</li>
* <li>Prompt content (instruction)</li>
* <li>Newline separator</li>
* <li>Prompt identifier marker (e.g., "--- Prompt-ID: filename ---")</li>
* <li>Newline separator</li>
* <li>Document text section marker</li>
* <li>Newline separator</li>
* <li>Document text content</li>
* <li>Newline separator</li>
* <li>Response format specification (JSON-only with required fields)</li>
* </ol>
* <p>
* This fixed order ensures that:
* <ul>
* <li>The prompt guides the AI</li>
* <li>The document text is clearly separated and identified</li>
* <li>Another implementation knows exactly where each part begins</li>
* <li>The composition is deterministic and reproducible</li>
* <li>The response format expectation is explicit in the actual request</li>
* <li>Another implementation knows exactly where each part begins and ends</li>
* <li>The composition is deterministic and reproducible across runs</li>
* </ul>
*
* @param promptIdentifier the stable identifier for this prompt; must not be null
@@ -71,17 +85,19 @@ public class AiRequestComposer {
Objects.requireNonNull(promptContent, "promptContent must not be null");
Objects.requireNonNull(documentText, "documentText must not be null");
// The complete request text is composed in a deterministic order:
// The complete request text is composed in a fixed, deterministic order:
// 1. Prompt content (instruction)
// 2. Newline separator
// 3. Prompt identifier (for reference)
// 3. Prompt identifier marker (for traceability)
// 4. Newline separator
// 5. Document text section marker
// 6. Newline separator
// 7. Document text content
// 8. Newline separator
// 9. Response format specification (JSON-only with required fields)
//
// This order is fixed so that another implementation knows exactly where
// the prompt and document text are positioned.
// each part is positioned and what to expect.
StringBuilder requestBuilder = new StringBuilder();
requestBuilder.append(promptContent);
requestBuilder.append("\n");
@@ -90,6 +106,8 @@ public class AiRequestComposer {
requestBuilder.append("--- Document Text ---");
requestBuilder.append("\n");
requestBuilder.append(documentText);
requestBuilder.append("\n");
appendJsonResponseFormat(requestBuilder);
// Record the exact character count of the document text that was included.
// This is the length of the document text (not the complete request).
@@ -107,12 +125,13 @@ public class AiRequestComposer {
* <p>
* This is a helper method that builds the exact string that would be included in the
* HTTP request to the AI service. It follows the same deterministic order as
* {@link #compose(PromptIdentifier, String, String)}.
* {@link #compose(PromptIdentifier, String, String)}, including the explicit
* JSON-only response format specification.
*
* @param promptIdentifier the stable identifier for this prompt; must not be null
* @param promptContent the prompt template content; must not be null
* @param documentText the extracted document text; must not be null
* @return the complete, deterministically-ordered request text for the AI
* @return the complete, deterministically-ordered request text for the AI (includes JSON format spec)
* @throws NullPointerException if any parameter is null
*/
public static String buildCompleteRequestText(
@@ -132,10 +151,37 @@ public class AiRequestComposer {
requestBuilder.append("--- Document Text ---");
requestBuilder.append("\n");
requestBuilder.append(documentText);
requestBuilder.append("\n");
appendJsonResponseFormat(requestBuilder);
return requestBuilder.toString();
}
/**
* Appends the explicit JSON-only response format specification to the request.
* <p>
* This ensures that the AI knows it must respond with a JSON object containing
* exactly these fields: {@code title}, {@code reasoning}, and optionally {@code date}.
* <p>
* This specification is part of the deterministic composition and is included in
* the actual request text sent to the AI service.
*
* @param requestBuilder the StringBuilder to append the format specification to
*/
private static void appendJsonResponseFormat(StringBuilder requestBuilder) {
requestBuilder.append("--- Response Format (JSON-only) ---");
requestBuilder.append("\n");
requestBuilder.append("Respond with a JSON object containing exactly:");
requestBuilder.append("\n");
requestBuilder.append(" \"title\": string (mandatory, max 20 characters, base title only)");
requestBuilder.append("\n");
requestBuilder.append(" \"reasoning\": string (mandatory, explanation of the decision)");
requestBuilder.append("\n");
requestBuilder.append(" \"date\": string (optional, YYYY-MM-DD format if provided)");
requestBuilder.append("\n");
requestBuilder.append("No text outside the JSON object.");
}
private AiRequestComposer() {
// Static utility class no instances
}

View File

@@ -192,4 +192,70 @@ class AiRequestComposerTest {
assertThat(result).contains("Prompt\nwith\nnewlines");
assertThat(result).contains("Document\ntext");
}
@Test
void buildCompleteRequestText_shouldIncludeJsonResponseFormat() {
// Given
PromptIdentifier promptId = new PromptIdentifier("prompt.txt");
String promptContent = "Analyze the document.";
String documentText = "Sample document content";
// When
String result = AiRequestComposer.buildCompleteRequestText(promptId, promptContent, documentText);
// Then
// Verify that the actual composed request includes JSON response format specification
assertThat(result).contains("Response Format (JSON-only)");
assertThat(result).contains("\"title\"");
assertThat(result).contains("\"reasoning\"");
assertThat(result).contains("\"date\"");
assertThat(result).contains("mandatory");
assertThat(result).contains("optional");
assertThat(result).contains("No text outside the JSON object");
}
@Test
void buildCompleteRequestText_shouldHaveDeterministicJsonFormatOrder() {
// Given
PromptIdentifier promptId = new PromptIdentifier("prompt.txt");
String promptContent = "Process this.";
String documentText = "Document to process";
// When
String result = AiRequestComposer.buildCompleteRequestText(promptId, promptContent, documentText);
// Then
// Verify deterministic order: document text comes before JSON response format
int documentTextIndex = result.indexOf(documentText);
int jsonFormatIndex = result.indexOf("Response Format (JSON-only)");
assertThat(documentTextIndex).isLessThan(jsonFormatIndex);
// Verify JSON field order: title before reasoning, reasoning before date
int titleIndex = result.indexOf("\"title\"");
int reasoningIndex = result.indexOf("\"reasoning\"");
int dateIndex = result.indexOf("\"date\"");
assertThat(titleIndex).isLessThan(reasoningIndex);
assertThat(reasoningIndex).isLessThan(dateIndex);
}
@Test
void buildCompleteRequestText_shouldProduceCompleteRequest_withAllSections() {
// Given
PromptIdentifier promptId = new PromptIdentifier("prompt_v3.txt");
String promptContent = "Classify the document.";
String documentText = "Content to classify";
// When
String result = AiRequestComposer.buildCompleteRequestText(promptId, promptContent, documentText);
// Then
// Verify all sections are present in the actual composed request
assertThat(result)
.contains("Classify the document.") // prompt
.contains("--- Prompt-ID: prompt_v3.txt ---") // identifier
.contains("--- Document Text ---") // document marker
.contains("Content to classify") // document text
.contains("--- Response Format (JSON-only) ---") // JSON format marker
.contains("Respond with a JSON object containing exactly:"); // JSON instruction
}
}