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 -- "); 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 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; } // Schema auf gültige JSON-Syntax prüfen. using var schemaDocument = JsonDocument.Parse(schema); // Den ursprünglichen Prüfbericht für den späteren Abgleich einlesen. var reportOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { RespectRequiredConstructorParameters = true, RespectNullableAnnotations = true }; var sourceReport = JsonSerializer.Deserialize( report, reportOptions); if (sourceReport is null || sourceReport.Rows.Any(row => row is null) || sourceReport.Status is not ("Completed" or "Rejected")) { Console.Error.WriteLine( "Der Eingabebericht enthält keine unterstützte Berichtsstruktur."); 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( 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, sourceReport); 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( $"JSON konnte nicht gelesen werden oder passt nicht zum 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; }