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; } 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); 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; }