Fix OpenAI-Adapter: extrahiert choices[0].message.content zweistufig
Die OpenAI Chat Completions API liefert den eigentlichen KI-Inhalt als escaped JSON-String in choices[0].message.content, nicht als direktes JSON-Objekt. Der Adapter gab bisher den gesamten Envelope zurück, was dazu führte, dass AiResponseParser das Pflichtfeld 'title' nicht fand. Neues Verhalten: extractContentFromResponse() parst zunächst den äußeren Envelope und gibt choices[0].message.content als AiRawResponse-Inhalt weiter – analog zum AnthropicClaudeHttpAdapter. Bei fehlendem Inhalt (leer, kein choices-Array) oder unparseablem Envelope wird eine technische Failure (NO_CHOICE_CONTENT bzw. UNPARSEABLE_JSON) zurückgegeben. Tests aktualisiert und drei neue Tests für den zweistufigen Parse-Pfad sowie für Fehlerfälle ergänzt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+56
-4
@@ -9,6 +9,8 @@ import java.util.Objects;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;
|
||||
@@ -61,9 +63,11 @@ import de.gecheckt.pdf.umbenenner.domain.model.AiRequestRepresentation;
|
||||
* <p>
|
||||
* <strong>Response handling:</strong>
|
||||
* <ul>
|
||||
* <li><strong>HTTP 200:</strong> Returns {@link AiInvocationSuccess} with the raw response body,
|
||||
* even if the body is invalid JSON or semantically problematic. The Application layer
|
||||
* is responsible for parsing and validating content.</li>
|
||||
* <li><strong>HTTP 200:</strong> Extracts {@code choices[0].message.content} from the
|
||||
* OpenAI Chat Completions response envelope and returns it as the raw response content
|
||||
* via {@link AiInvocationSuccess}. The Application layer then parses and validates
|
||||
* this content as a NamingProposal JSON object. If the envelope cannot be parsed or
|
||||
* the expected content field is absent, a technical failure is returned.</li>
|
||||
* <li><strong>HTTP non-200:</strong> Treated as a technical failure. The response body may
|
||||
* contain an error message, but this is logged for debugging; the client treats it as
|
||||
* a transient communication failure.</li>
|
||||
@@ -77,6 +81,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.AiRequestRepresentation;
|
||||
* <li>Endpoint unreachable (connection refused, DNS failure, etc.)</li>
|
||||
* <li>Interrupted IO during HTTP communication</li>
|
||||
* <li>HTTP response with non-2xx status code</li>
|
||||
* <li>HTTP 200 response body that cannot be parsed as JSON ({@code UNPARSEABLE_JSON})</li>
|
||||
* <li>HTTP 200 response with no {@code choices} or no {@code message.content}
|
||||
* ({@code NO_CHOICE_CONTENT})</li>
|
||||
* <li>Any other transport-level exception</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
@@ -184,7 +191,7 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
|
||||
HttpResponse<String> response = executeRequest(httpRequest);
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
return new AiInvocationSuccess(request, new AiRawResponse(response.body()));
|
||||
return extractContentFromResponse(request, response.body());
|
||||
} else {
|
||||
String reason = "HTTP_" + response.statusCode();
|
||||
String message = "AI service returned status " + response.statusCode();
|
||||
@@ -220,6 +227,51 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the NamingProposal content from a successful (HTTP 200) OpenAI response.
|
||||
* <p>
|
||||
* The OpenAI Chat Completions response wraps the actual AI content inside an envelope:
|
||||
* {@code choices[0].message.content}. This content string is the NamingProposal JSON
|
||||
* that the Application layer expects. This method performs the two-step extraction:
|
||||
* first parses the outer envelope, then returns the inner content string.
|
||||
* <p>
|
||||
* If the envelope JSON cannot be parsed, or if {@code choices} is absent or empty,
|
||||
* or if {@code message.content} is absent or blank, a technical failure is returned.
|
||||
*
|
||||
* @param request the original request (carried through to the result)
|
||||
* @param responseBody the raw HTTP response body (the OpenAI envelope)
|
||||
* @return success with the extracted content string, or a technical failure
|
||||
*/
|
||||
private AiInvocationResult extractContentFromResponse(AiRequestRepresentation request, String responseBody) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(responseBody);
|
||||
JSONArray choices = json.optJSONArray("choices");
|
||||
if (choices == null || choices.isEmpty()) {
|
||||
LOG.warn("OpenAI response contained no choices");
|
||||
return new AiInvocationTechnicalFailure(request, "NO_CHOICE_CONTENT",
|
||||
"OpenAI response contained no choices");
|
||||
}
|
||||
JSONObject firstChoice = choices.getJSONObject(0);
|
||||
JSONObject message = firstChoice.optJSONObject("message");
|
||||
if (message == null) {
|
||||
LOG.warn("OpenAI response choice contained no message");
|
||||
return new AiInvocationTechnicalFailure(request, "NO_CHOICE_CONTENT",
|
||||
"OpenAI response choice contained no message");
|
||||
}
|
||||
String content = message.optString("content", null);
|
||||
if (content == null || content.isBlank()) {
|
||||
LOG.warn("OpenAI response message.content is absent or blank");
|
||||
return new AiInvocationTechnicalFailure(request, "NO_CHOICE_CONTENT",
|
||||
"OpenAI response message.content is absent or blank");
|
||||
}
|
||||
return new AiInvocationSuccess(request, new AiRawResponse(content));
|
||||
} catch (JSONException e) {
|
||||
LOG.warn("OpenAI response could not be parsed as JSON: {}", e.getMessage());
|
||||
return new AiInvocationTechnicalFailure(request, "UNPARSEABLE_JSON",
|
||||
"OpenAI response body is not valid JSON: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an OpenAI Chat Completions API request from the request representation.
|
||||
* <p>
|
||||
|
||||
Reference in New Issue
Block a user