using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace Nop.Plugin.Misc.FruitBankPlugin.Services; /// /// Szöveges üzenetet parse-ol: kinyeri a szállítási dátumokat és a termékigényeket. /// Ha egy üzenetben több dátum szerepel, minden dátumhoz külön ParsedDraftGroup-ot ad vissza. /// public class OrderDraftParserService { private readonly OpenAIApiService _openAiService; // Hány napnál távolabbi dátum számít előrendelésnek public const int PreorderThresholdDays = 4; public OrderDraftParserService(OpenAIApiService openAiService) { _openAiService = openAiService; } /// /// Egy raw üzenetből kinyeri a (dátum → termékigények) csoportokat. /// Minden visszaadott csoport egy külön OrderDraft-nak felel meg. /// public async Task> ParseMessageAsync(string rawMessage) { var systemPrompt = """ Te egy magyar gyümölcs- és zöldségnagyker rendelési asszisztense vagy. Egy beérkező szöveges üzenetből ki kell nyerned a rendelési szándékokat. FELADATOD: 1. Azonosítsd az összes szállítási dátumot az üzenetben. 2. Csoportosítsd a termékigényeket dátum szerint. 3. Ha egy tételhez nincs explicit dátum megadva, rendeld a legközelebbi említett dátumhoz. 4. Ha egyáltalán nincs dátum az üzenetben, használd a mai napot + 1 nap értéket. 5. A mennyiségnél mindig rekeszben gondolkodj (ha nem egyértelmű, rekesz az alapértelmezett). 6. A termék nevét pontosan úgy add vissza, ahogy az üzenetben szerepel (ne fordítsd, ne értelmezd). VÁLASZ FORMÁTUM (csak JSON, semmi más): { "groups": [ { "deliveryDate": "YYYY-MM-DD", "items": [ { "rawProductText": "az eredeti szöveg ahogy szerepelt", "requestedQuantity": 10, "sortOrder": 1 } ] } ] } FONTOS: - Csak JSON-t adj vissza, semmilyen magyarázatot, bevezető szöveget vagy markdown blokkot ne írj. - Ha a dátum relatív (pl. "jövő héten", "jövő szerdán", "holnap"), számítsd ki a tényleges dátumot a mai naphoz képest. - Mai dátum: """ + " " + DateTime.Today.ToString("yyyy-MM-dd"); var userPrompt = $"Üzenet:\n{rawMessage}"; var response = await _openAiService.GetSimpleResponseAsync(systemPrompt, userPrompt); if (string.IsNullOrWhiteSpace(response)) return []; try { // Tisztítjuk a markdown code fences-t ha mégis beleírta var clean = response .Replace("```json", "") .Replace("```", "") .Trim(); var parsed = JsonSerializer.Deserialize(clean, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return parsed?.Groups ?? []; } catch (Exception ex) { Console.Error.WriteLine($"[OrderDraftParser] JSON parse error: {ex.Message}\nRaw response: {response}"); return []; } } /// /// Egy termék raw szövegéből meghatározza a kanonikus terméknevet és /// a NopCommerce kereséshez használható kulcsszavakat. /// public async Task ResolveProductNameAsync(string rawProductText) { var systemPrompt = """ Te egy magyar gyümölcs- és zöldségnagyker asszisztense vagy. Egy vevő által írt termékmegnevezésből kell meghatároznod: 1. Mi a termék pontos, kanonikus neve magyarul (pl. "piros kalif" → "Kaliforniai paprika piros") 2. Milyen kulcsszavakkal érdemes keresni a termékadatbázisban (pl. ["paprika", "kaliforniai", "piros"]) VÁLASZ FORMÁTUM (csak JSON): { "canonicalName": "Kaliforniai paprika piros", "searchKeywords": ["paprika", "kaliforniai", "piros"] } Csak JSON-t adj vissza. """; var response = await _openAiService.GetSimpleResponseAsync(systemPrompt, rawProductText); if (string.IsNullOrWhiteSpace(response)) return new ProductResolutionHint { CanonicalName = rawProductText, SearchKeywords = [rawProductText] }; try { var clean = response.Replace("```json", "").Replace("```", "").Trim(); var hint = JsonSerializer.Deserialize(clean, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return hint ?? new ProductResolutionHint { CanonicalName = rawProductText, SearchKeywords = [rawProductText] }; } catch { return new ProductResolutionHint { CanonicalName = rawProductText, SearchKeywords = [rawProductText] }; } } /// /// Több termék ID-t sorrendbe rendez az AI segítségével a raw szöveg és a terméknevek alapján. /// A historikus mapping által már előre rangsorolt lista végső finomhangolása. /// public async Task> RankCandidatesAsync( string rawProductText, string canonicalName, List<(int Id, string Name)> candidates) { if (candidates.Count <= 1) return candidates.Select(c => c.Id).ToList(); var candidateList = candidates .Select((c, i) => $"{i + 1}. ID={c.Id} Név=\"{c.Name}\"") .Aggregate(new StringBuilder(), (sb, s) => sb.AppendLine(s)) .ToString(); var systemPrompt = """ Te egy magyar gyümölcs- és zöldségnagyker asszisztense vagy. Rendezd sorrendbe a termékkandidátusokat aszerint, hogy melyik felel meg legjobban a keresett terméknek. Az első legyen a legjobb találat. VÁLASZ FORMÁTUM (csak JSON, semmi más): { "rankedIds": [123, 456, 789] } """; var userPrompt = $""" Keresett termék (eredeti szöveg): "{rawProductText}" Kanonikus név: "{canonicalName}" Kandid\u00e1tusok: {candidateList} """; var response = await _openAiService.GetSimpleResponseAsync(systemPrompt, userPrompt); if (string.IsNullOrWhiteSpace(response)) return candidates.Select(c => c.Id).ToList(); try { var clean = response.Replace("```json", "").Replace("```", "").Trim(); using var doc = JsonDocument.Parse(clean); var ranked = doc.RootElement .GetProperty("rankedIds") .EnumerateArray() .Select(e => e.GetInt32()) .ToList(); // Csak valódi kandidátus ID-kat tartunk meg, a maradékot hozzáfűzzük var validIds = ranked.Where(id => candidates.Any(c => c.Id == id)).ToList(); var missing = candidates.Select(c => c.Id).Except(validIds).ToList(); return [.. validIds, .. missing]; } catch { return candidates.Select(c => c.Id).ToList(); } } } // ── DTO-k ────────────────────────────────────────────────────────────────── public class ParsedMessageResponse { [JsonPropertyName("groups")] public List Groups { get; set; } = []; } public class ParsedDraftGroup { [JsonPropertyName("deliveryDate")] public string DeliveryDate { get; set; } = string.Empty; [JsonPropertyName("items")] public List Items { get; set; } = []; public DateTime GetDeliveryDateTime() => DateTime.TryParse(DeliveryDate, out var dt) ? dt : DateTime.Today.AddDays(1); } public class ParsedDraftItem { [JsonPropertyName("rawProductText")] public string RawProductText { get; set; } = string.Empty; [JsonPropertyName("requestedQuantity")] public int RequestedQuantity { get; set; } = 1; [JsonPropertyName("sortOrder")] public int SortOrder { get; set; } } public class ProductResolutionHint { [JsonPropertyName("canonicalName")] public string CanonicalName { get; set; } = string.Empty; [JsonPropertyName("searchKeywords")] public List SearchKeywords { get; set; } = []; }