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 loaded prompt content (from the external prompt file)</li>
|
||||||
* <li>The stable prompt identifier (derived from the prompt source)</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>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>
|
* <li>The exact character count that was sent (for traceability)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
* <strong>Order and structure:</strong> The composition follows a fixed, documented order
|
* <strong>Composition order:</strong> The request follows a fixed, documented order:
|
||||||
* to ensure that another implementation would not need to guess how the prompt and document
|
* <ol>
|
||||||
* are combined. The prompt is presented first, followed by the document text, with clear
|
* <li>Prompt content (instruction)</li>
|
||||||
* structural markers to distinguish between them.
|
* <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>
|
* <p>
|
||||||
* <strong>JSON-only response expectation:</strong> The request is constructed with the
|
* This fixed order ensures reproducibility and allows another implementation to
|
||||||
* explicit expectation that the AI will respond with a JSON object containing:
|
* 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>
|
* <ul>
|
||||||
* <li>{@code title} — mandatory, max 20 characters (base title)</li>
|
* <li>{@code title} — mandatory, max 20 characters (base title)</li>
|
||||||
* <li>{@code reasoning} — mandatory, the AI's explanation</li>
|
* <li>{@code reasoning} — mandatory, the AI's explanation</li>
|
||||||
@@ -38,22 +47,27 @@ public class AiRequestComposer {
|
|||||||
/**
|
/**
|
||||||
* Composes a deterministic AI request representation.
|
* Composes a deterministic AI request representation.
|
||||||
* <p>
|
* <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>
|
* <ol>
|
||||||
* <li>Prompt content</li>
|
* <li>Prompt content (instruction)</li>
|
||||||
* <li>Separator: newline</li>
|
* <li>Newline separator</li>
|
||||||
* <li>Prompt identifier (for reference/traceability)</li>
|
* <li>Prompt identifier marker (e.g., "--- Prompt-ID: filename ---")</li>
|
||||||
* <li>Separator: newline</li>
|
* <li>Newline separator</li>
|
||||||
* <li>Document text section marker</li>
|
* <li>Document text section marker</li>
|
||||||
|
* <li>Newline separator</li>
|
||||||
* <li>Document text content</li>
|
* <li>Document text content</li>
|
||||||
|
* <li>Newline separator</li>
|
||||||
|
* <li>Response format specification (JSON-only with required fields)</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
* <p>
|
* <p>
|
||||||
* This fixed order ensures that:
|
* This fixed order ensures that:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>The prompt guides the AI</li>
|
* <li>The prompt guides the AI</li>
|
||||||
* <li>The document text is clearly separated and identified</li>
|
* <li>The document text is clearly separated and identified</li>
|
||||||
* <li>Another implementation knows exactly where each part begins</li>
|
* <li>The response format expectation is explicit in the actual request</li>
|
||||||
* <li>The composition is deterministic and reproducible</li>
|
* <li>Another implementation knows exactly where each part begins and ends</li>
|
||||||
|
* <li>The composition is deterministic and reproducible across runs</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param promptIdentifier the stable identifier for this prompt; must not be null
|
* @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(promptContent, "promptContent must not be null");
|
||||||
Objects.requireNonNull(documentText, "documentText 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)
|
// 1. Prompt content (instruction)
|
||||||
// 2. Newline separator
|
// 2. Newline separator
|
||||||
// 3. Prompt identifier (for reference)
|
// 3. Prompt identifier marker (for traceability)
|
||||||
// 4. Newline separator
|
// 4. Newline separator
|
||||||
// 5. Document text section marker
|
// 5. Document text section marker
|
||||||
// 6. Newline separator
|
// 6. Newline separator
|
||||||
// 7. Document text content
|
// 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
|
// 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();
|
StringBuilder requestBuilder = new StringBuilder();
|
||||||
requestBuilder.append(promptContent);
|
requestBuilder.append(promptContent);
|
||||||
requestBuilder.append("\n");
|
requestBuilder.append("\n");
|
||||||
@@ -90,6 +106,8 @@ public class AiRequestComposer {
|
|||||||
requestBuilder.append("--- Document Text ---");
|
requestBuilder.append("--- Document Text ---");
|
||||||
requestBuilder.append("\n");
|
requestBuilder.append("\n");
|
||||||
requestBuilder.append(documentText);
|
requestBuilder.append(documentText);
|
||||||
|
requestBuilder.append("\n");
|
||||||
|
appendJsonResponseFormat(requestBuilder);
|
||||||
|
|
||||||
// Record the exact character count of the document text that was included.
|
// Record the exact character count of the document text that was included.
|
||||||
// This is the length of the document text (not the complete request).
|
// This is the length of the document text (not the complete request).
|
||||||
@@ -107,12 +125,13 @@ public class AiRequestComposer {
|
|||||||
* <p>
|
* <p>
|
||||||
* This is a helper method that builds the exact string that would be included in the
|
* 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
|
* 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 promptIdentifier the stable identifier for this prompt; must not be null
|
||||||
* @param promptContent the prompt template content; must not be null
|
* @param promptContent the prompt template content; must not be null
|
||||||
* @param documentText the extracted document text; 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
|
* @throws NullPointerException if any parameter is null
|
||||||
*/
|
*/
|
||||||
public static String buildCompleteRequestText(
|
public static String buildCompleteRequestText(
|
||||||
@@ -132,10 +151,37 @@ public class AiRequestComposer {
|
|||||||
requestBuilder.append("--- Document Text ---");
|
requestBuilder.append("--- Document Text ---");
|
||||||
requestBuilder.append("\n");
|
requestBuilder.append("\n");
|
||||||
requestBuilder.append(documentText);
|
requestBuilder.append(documentText);
|
||||||
|
requestBuilder.append("\n");
|
||||||
|
appendJsonResponseFormat(requestBuilder);
|
||||||
|
|
||||||
return requestBuilder.toString();
|
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() {
|
private AiRequestComposer() {
|
||||||
// Static utility class – no instances
|
// Static utility class – no instances
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,4 +192,70 @@ class AiRequestComposerTest {
|
|||||||
assertThat(result).contains("Prompt\nwith\nnewlines");
|
assertThat(result).contains("Prompt\nwith\nnewlines");
|
||||||
assertThat(result).contains("Document\ntext");
|
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