M5 AP-002 JSON-Only-Erwartung in KI-Anfrage ergänzt und Tests geschärft
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user