Add structured AI analysis output and invoice ID validation

This commit is contained in:
admin committed 2026-09-11 14:33:31 +02:00
1 parent 12bc2a1822
commit 5b697ee200
8 files changed
+298 -16

No files matched your search

+80 -9
View File
@@ -2,6 +2,9 @@
using Azure.AI.OpenAI;
using Microsoft.Extensions.Configuration;
using OpenAI.Chat;
using System.Text.Json;
using System.Text.Json.Serialization;
using ImportAssist.Core;
if (args.Length != 1)
{
@@ -38,6 +41,9 @@ if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var endpointUri) ||
try
{
var schema = await File.ReadAllTextAsync(
"docs/ai-analysis.schema.json");
var instructions = await File.ReadAllTextAsync(
"docs/ai-analysis-prompt.md");
@@ -78,7 +84,11 @@ try
var options = new ChatCompletionOptions
{
MaxOutputTokenCount = 2000
MaxOutputTokenCount = 2000,
ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
jsonSchemaFormatName: "invoice_analysis",
jsonSchema: BinaryData.FromString(schema),
jsonSchemaIsStrict: true)
};
Console.Error.WriteLine($"Analyse mit {deployment} läuft ...");
@@ -88,17 +98,17 @@ try
ChatCompletion completion = await chatClient.CompleteChatAsync(
messages, options, timeout.Token);
foreach (var part in completion.Content)
{
Console.Write(part.Text);
}
Console.WriteLine();
Console.Error.WriteLine(
$"Tokens: Eingabe {completion.Usage.InputTokenCount}, " +
$"Ausgabe {completion.Usage.OutputTokenCount}");
if (!string.IsNullOrWhiteSpace(completion.Refusal))
{
Console.Error.WriteLine(
$"Das Modell hat die Analyse abgelehnt: {completion.Refusal}");
return 2;
}
if (completion.FinishReason != ChatFinishReason.Stop)
{
Console.Error.WriteLine(
@@ -106,6 +116,61 @@ try
return 2;
}
var responseJson = string.Concat(
completion.Content.Select(part => part.Text));
if (string.IsNullOrWhiteSpace(responseJson))
{
Console.Error.WriteLine("Das Modell hat keinen Antwortinhalt geliefert.");
return 2;
}
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = false,
RespectRequiredConstructorParameters = true,
RespectNullableAnnotations = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
WriteIndented = true
};
var analysis = JsonSerializer.Deserialize<InvoiceAnalysis>(
responseJson, jsonOptions);
if (analysis is null)
{
Console.Error.WriteLine("Die Antwort enthält kein Analyseobjekt.");
return 2;
}
// Nullwerte innerhalb von Arrays werden durch die
// Nullable-Einstellung des Serializers nicht vollständig geprüft.
if (analysis.Findings.Any(finding =>
finding is null ||
finding.MissingInformation.Any(item => item is null)) ||
analysis.Limitations.Any(item => item is null))
{
Console.Error.WriteLine("Die Antwort enthält unzulässige Null-Einträge.");
return 2;
}
var validationErrors = InvoiceAnalysisValidator.Validate(analysis);
if (validationErrors.Count > 0)
{
Console.Error.WriteLine(
"Die Modellantwort verletzt den Analysevertrag:");
foreach (var error in validationErrors)
{
Console.Error.WriteLine(error);
}
return 2;
}
Console.WriteLine(JsonSerializer.Serialize(analysis, jsonOptions));
return 0;
}
catch (ClientResultException ex)
@@ -114,6 +179,12 @@ catch (ClientResultException ex)
$"Azure-Modellaufruf fehlgeschlagen. HTTP-Status: {ex.Status}");
return 1;
}
catch (JsonException ex)
{
Console.Error.WriteLine(
$"Die JSON-Antwort passt nicht zum erwarteten C#-Datenmodell: {ex.Message}");
return 2;
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Die Analyse hat das Zeitlimit überschritten.");
@@ -128,4 +199,4 @@ catch (UnauthorizedAccessException)
{
Console.Error.WriteLine("Keine Leseberechtigung für eine Eingabedatei.");
return 1;
}
}