From 12bc2a1822cf6f4260e70ebb97908226a91a3db8 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Thu, 10 Sep 2026 11:24:45 +0200 Subject: [PATCH] Add C# Azure AI analysis tool and document evaluation runs --- ImportAssist.slnx | 1 + README.md | 48 +++++++ docs/ai-evaluation-v1.md | 41 +++++- .../ImportAssist.Analyze.csproj | 16 +++ tools/ImportAssist.Analyze/Program.cs | 131 ++++++++++++++++++ 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 tools/ImportAssist.Analyze/ImportAssist.Analyze.csproj create mode 100644 tools/ImportAssist.Analyze/Program.cs diff --git a/ImportAssist.slnx b/ImportAssist.slnx index 5f2fc31..48fa8a7 100644 --- a/ImportAssist.slnx +++ b/ImportAssist.slnx @@ -10,6 +10,7 @@ + diff --git a/README.md b/README.md index d815840..41d9b03 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,51 @@ Azurite speichert seine Daten im angegebenen lokalen Verzeichnis. Lokaler Prototyp mit synthetischen Daten. Cloud-Deployment und KI-Anbindung stehen noch aus. + +## KI-Analyse aus C# + +`ImportAssist.Analyze` liest Analyseprompt, Schnittstellenbeschreibung +und einen gespeicherten JSON-Prüfbericht ein und ruft Azure OpenAI auf. + +### Lokale Konfiguration + +User Secrets für das Projekt initialisieren: + +```bash +dotnet user-secrets init --project tools/ImportAssist.Analyze +``` + +Folgende Konfigurationswerte im zugehörigen User-Secrets-Speicher setzen: + +- AzureOpenAI:Endpoint – Azure-OpenAI-Basisadresse der Ressource +- AzureOpenAI:Deployment – importassist-analysis +- AzureOpenAI:ApiKey – Schlüssel der Ressource + +API-Schlüssel ausschließlich lokal hinterlegen. +User Secrets liegen außerhalb des Repositorys, sind aber unverschlüsselt. + +### Ausführen + +Alle Befehle aus dem Repository-Hauptverzeichnis ausführen: + +```bash +dotnet run --project tools/ImportAssist.Analyze -- \ + samples/invoices-report.json + +dotnet run --no-build --project tools/ImportAssist.Analyze -- \ + samples/invoices-rejected-report.json +``` + +Azurite und der Functions-Host werden für diese gespeicherten Berichte +nicht benötigt. Der Modellaufruf benötigt eine Internetverbindung. + +Die Analyse erscheint auf stdout; Statusmeldungen und Tokenverbrauch +erscheinen auf stderr. + +Exitcodes: +- 0: Modellantwort regulär abgeschlossen; keine fachliche Qualitätsgarantie. +- 1: Konfigurations-, Datei- oder Aufruffehler. +- 2: Modellantwort nicht regulär abgeschlossen. + +Die Antwort ist Freitext. Eine automatische fachliche Bewertung und +ein verbindliches JSON-Ausgabeformat sind noch nicht implementiert. \ No newline at end of file diff --git a/docs/ai-evaluation-v1.md b/docs/ai-evaluation-v1.md index cb31120..491e9ac 100644 --- a/docs/ai-evaluation-v1.md +++ b/docs/ai-evaluation-v1.md @@ -135,4 +135,43 @@ Dateistrukturfehler in Zeile 3. ### Bewertung Teilweise bestanden. Kernaussage korrekt; Korrekturvorschlag und Wortwahl benötigen -Präzisierung. Eine Quellenzuordnung bleibt offen. \ No newline at end of file +Präzisierung. Eine Quellenzuordnung bleibt offen. + +## C#-Testläufe vom 10.09.2026 + +### Konfiguration +- Tool: ImportAssist.Analyze +- SDK: .NET 10.0.112 +- Azure.AI.OpenAI: 2.1.0 +- Modell: gpt-4.1-mini, Version 2025-04-14 +- Bereitstellung: importassist-analysis +- Eingaben: aktueller Analyseprompt, Schnittstellenspezifikation + und jeweiliger JSON-Prüfbericht +- Maximale Ausgabetokens: 2000 +- Jeder Aufruf ohne vorherigen Chatverlauf und ohne Tools + +### Completed-Bericht +- Fehlerzeilen und Regelkennungen korrekt zugeordnet. +- Keine unbelegte Importfreigabe. +- Währung korrekt als ungeprüft behandelt. +- Ungenaue Formulierung: Nur eine Rechnung sei „validiert worden“. + Tatsächlich wurden fünf geprüft; eine bestand die Prüfregeln. +- Gutschrift als möglicher Prüfansatz erwähnt, nicht als Tatsache. +- Tokenverbrauch: 1434 Eingabe, 605 Ausgabe. + +Bewertung: teilweise bestanden; sprachliche Unschärfe verbleibt. + +### Rejected-Bericht +- Strukturfehler in Zeile 3 korrekt CSV-001 zugeordnet. +- Ausgebliebene fachliche Prüfung korrekt erklärt. +- Keine konkrete fehlende Spalte erfunden. +- Originalinhalt der betroffenen Zeile als fehlende Information benannt. +- Prüfung der Quelldaten statt unbegründeter Korrektur empfohlen. +- Tokenverbrauch: 1202 Eingabe, 426 Ausgabe. + +Bewertung: für diesen Testlauf bestanden. + +### Grenzen +Je ein manueller C#-Testlauf pro Fall. +Keine Aussage über zuverlässige Wiederholbarkeit oder andere Eingaben. +Die Antworten werden bislang als Freitext ausgegeben und manuell geprüft. \ No newline at end of file diff --git a/tools/ImportAssist.Analyze/ImportAssist.Analyze.csproj b/tools/ImportAssist.Analyze/ImportAssist.Analyze.csproj new file mode 100644 index 0000000..364882f --- /dev/null +++ b/tools/ImportAssist.Analyze/ImportAssist.Analyze.csproj @@ -0,0 +1,16 @@ + + + + Exe + net10.0 + enable + enable + b94ecd7c-a44e-42f7-960b-3a8a6238991c + + + + + + + + diff --git a/tools/ImportAssist.Analyze/Program.cs b/tools/ImportAssist.Analyze/Program.cs new file mode 100644 index 0000000..fdece0e --- /dev/null +++ b/tools/ImportAssist.Analyze/Program.cs @@ -0,0 +1,131 @@ +using System.ClientModel; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Configuration; +using OpenAI.Chat; + +if (args.Length != 1) +{ + Console.Error.WriteLine( + "Aufruf aus dem Repository-Hauptverzeichnis: " + + "dotnet run --project tools/ImportAssist.Analyze -- "); + return 1; +} + +var configuration = new ConfigurationBuilder() + .AddUserSecrets() + .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 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 + }; + + 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); + + 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 (completion.FinishReason != ChatFinishReason.Stop) + { + Console.Error.WriteLine( + $"Antwort nicht regulär abgeschlossen: {completion.FinishReason}"); + return 2; + } + + return 0; +} +catch (ClientResultException ex) +{ + Console.Error.WriteLine( + $"Azure-Modellaufruf fehlgeschlagen. HTTP-Status: {ex.Status}"); + return 1; +} +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; +} \ No newline at end of file