203 lines
5.7 KiB
C#
203 lines
5.7 KiB
C#
using System.ClientModel;
|
|
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)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Aufruf aus dem Repository-Hauptverzeichnis: " +
|
|
"dotnet run --project tools/ImportAssist.Analyze -- <Prüfbericht.json>");
|
|
return 1;
|
|
}
|
|
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddUserSecrets<Program>()
|
|
.Build();
|
|
|
|
var endpoint = configuration["AzureOpenAI:Endpoint"];
|
|
var deployment = configuration["AzureOpenAI:Deployment"];
|
|
var apiKey = configuration["AzureOpenAI:ApiKey"];
|
|
|
|
if (string.IsNullOrWhiteSpace(endpoint) ||
|
|
string.IsNullOrWhiteSpace(deployment) ||
|
|
string.IsNullOrWhiteSpace(apiKey))
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Konfiguration fehlt: Endpoint, Deployment und ApiKey " +
|
|
"müssen in den User Secrets gesetzt sein.");
|
|
return 1;
|
|
}
|
|
|
|
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var endpointUri) ||
|
|
endpointUri.Scheme != Uri.UriSchemeHttps)
|
|
{
|
|
Console.Error.WriteLine("Endpoint muss eine gültige HTTPS-Adresse sein.");
|
|
return 1;
|
|
}
|
|
|
|
try
|
|
{
|
|
var schema = await File.ReadAllTextAsync(
|
|
"docs/ai-analysis.schema.json");
|
|
|
|
var instructions = await File.ReadAllTextAsync(
|
|
"docs/ai-analysis-prompt.md");
|
|
|
|
var specification = await File.ReadAllTextAsync(
|
|
"docs/invoice-interface-v1.md");
|
|
|
|
var report = await File.ReadAllTextAsync(args[0]);
|
|
|
|
if (string.IsNullOrWhiteSpace(instructions) ||
|
|
string.IsNullOrWhiteSpace(specification) ||
|
|
string.IsNullOrWhiteSpace(report))
|
|
{
|
|
Console.Error.WriteLine("Eine der Eingabedateien ist leer.");
|
|
return 1;
|
|
}
|
|
|
|
var azureClient = new AzureOpenAIClient(
|
|
endpointUri, new ApiKeyCredential(apiKey));
|
|
|
|
var chatClient = azureClient.GetChatClient(deployment);
|
|
|
|
ChatMessage[] messages =
|
|
[
|
|
new SystemChatMessage(instructions),
|
|
new UserChatMessage($"""
|
|
Schnittstellenbeschreibung:
|
|
|
|
{specification}
|
|
|
|
Prüfbericht:
|
|
|
|
{report}
|
|
|
|
Analysiere diesen Prüfbericht gemäß deinen Anweisungen
|
|
und der beigefügten Schnittstellenbeschreibung.
|
|
""")
|
|
];
|
|
|
|
var options = new ChatCompletionOptions
|
|
{
|
|
MaxOutputTokenCount = 2000,
|
|
ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
|
|
jsonSchemaFormatName: "invoice_analysis",
|
|
jsonSchema: BinaryData.FromString(schema),
|
|
jsonSchemaIsStrict: true)
|
|
};
|
|
|
|
Console.Error.WriteLine($"Analyse mit {deployment} läuft ...");
|
|
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(90));
|
|
|
|
ChatCompletion completion = await chatClient.CompleteChatAsync(
|
|
messages, options, timeout.Token);
|
|
|
|
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(
|
|
$"Antwort nicht regulär abgeschlossen: {completion.FinishReason}");
|
|
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)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"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.");
|
|
return 1;
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
Console.Error.WriteLine($"Dateifehler: {ex.Message}");
|
|
return 1;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
Console.Error.WriteLine("Keine Leseberechtigung für eine Eingabedatei.");
|
|
return 1;
|
|
}
|