using System.Text; using System.Text.RegularExpressions; using GerbilManagerWebAPI.Ai; using GerbilManagerWebAPI.Models; using GerbilManagerWebAPI.SaleAd; using Microsoft.Extensions.Options; namespace GerbilManagerWebAPI.Inbox { /// /// INBOX-2: AI reply DRAFT for an incoming inquiry (human-in-the-loop — the /// breeder edits and sends herself, NEVER auto-send; send is INBOX-3). /// /// PRIVACY / data minimization (INBOX-architecture.md §privacy): the prompt /// carries ONLY the inquiry's own text (signatures/quoted history stripped), /// the sender's display name for the greeting, a short German context, and /// optionally the current ForSale animals (name + Farbschlag). NOT the /// contact DB, NOT other requests, NOT any addresses. /// public sealed class DraftReplyService(HttpClient http, IOptions options) { private readonly OpenAiChatClient _client = new(http, options); public sealed record ForSaleAnimal(string Name, string? Farbschlag); public Task DraftAsync( Request request, IReadOnlyList forSale, CancellationToken ct = default) => _client.CompleteAsync(BuildSystemPrompt(), BuildUserPrompt(request, forSale), ct); internal static string BuildSystemPrompt() => """ Du hilfst einer Hobby-Rennmaus-Züchterin („Zucht der kleinen Chaoten“), eine freundliche deutsche Antwort auf eine Anfrage zu ENTWERFEN. Sie liest den Entwurf, passt ihn an und versendet selbst. Regeln: - Ton: warm, persönlich, hilfsbereit — wie eine erfahrene Hobby-Züchterin, nicht wie ein Unternehmen. Anrede per „du“, Gruß mit dem Namen der anfragenden Person, falls bekannt. - KEINE Fakten erfinden: Verfügbarkeit, Tiere und Eigenschaften NUR aus den mitgelieferten Daten. Ist keine Abgabetier-Liste mitgeliefert oder passt nichts, verweise freundlich darauf, dass sie aktuelle Infos persönlich gibt. - Keine Preise/Schutzgebühren nennen und keine Adressen oder sonstige persönliche Daten — solche Details klärt sie selbst im Gespräch. - Gib NUR den Antworttext aus (ohne Betreff, ohne Erklärungen) und beende mit einem herzlichen Gruß ohne Namens-Signatur. """; internal static string BuildUserPrompt(Request request, IReadOnlyList forSale) { var sb = new StringBuilder(); sb.AppendLine("Entwirf eine Antwort auf folgende Anfrage:"); sb.AppendLine(); if (!string.IsNullOrWhiteSpace(request.FromName)) sb.AppendLine($"Von: {request.FromName}"); if (!string.IsNullOrWhiteSpace(request.Subject)) sb.AppendLine($"Betreff: {request.Subject}"); sb.AppendLine(); sb.AppendLine("Anfrage-Text:"); sb.AppendLine(StripQuotedText(request.BodyText ?? "")); if (forSale.Count > 0) { sb.AppendLine(); sb.AppendLine("Aktuell abzugebende Tiere (einzige zulässige Quelle für Verfügbarkeits-Aussagen):"); foreach (var a in forSale) { sb.AppendLine(string.IsNullOrWhiteSpace(a.Farbschlag) ? $"- {a.Name}" : $"- {a.Name} ({a.Farbschlag})"); } } return sb.ToString(); } // ── Signatur-/Zitat-Stripping (best effort, bewusst konservativ) ──── private static readonly Regex QuoteIntro = new( @"^Am .+ schrieb .+:\s*$", RegexOptions.Compiled); private static readonly Regex OutlookHeader = new( @"^(Von|Gesendet|An|Betreff):\s", RegexOptions.Compiled); /// /// Drops quoted history ('>'-lines, the German Gmail "Am … schrieb …:" /// intro, Outlook-style forwarded headers) and everything below a signature /// delimiter ("-- "). Keeps the sender's own words untouched. /// internal static string StripQuotedText(string body) { var lines = body.Replace("\r\n", "\n").Split('\n'); var kept = new List(); foreach (var line in lines) { var trimmed = line.TrimEnd(); if (trimmed == "--" || trimmed == "-- ") break; // signature delimiter: everything below is signature if (QuoteIntro.IsMatch(trimmed)) break; // quoted history follows if (trimmed.StartsWith('>')) continue; if (OutlookHeader.IsMatch(trimmed)) continue; kept.Add(line); } // collapse the whitespace the stripping may have left behind return string.Join("\n", kept).Trim(); } } }