Add structured AI analysis output and invoice ID validation
This commit is contained in:
1 parent
12bc2a1822
commit
5b697ee200
8 files changed
+298
-16
No files matched your search
@@ -13,4 +13,8 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ImportAssist.Core\ImportAssist.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user