Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Services/OrderDraftParserService.cs

231 lines
8.6 KiB
C#

using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Egy raw üzenetből kinyeri a (dátum → termékigények) csoportokat.
/// Minden visszaadott csoport egy külön OrderDraft-nak felel meg.
/// </summary>
public async Task<List<ParsedDraftGroup>> 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<ParsedMessageResponse>(clean, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return parsed?.Groups ?? [];
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OrderDraftParser] JSON parse error: {ex.Message}\nRaw response: {response}");
return [];
}
}
/// <summary>
/// Egy termék raw szövegéből meghatározza a kanonikus terméknevet és
/// a NopCommerce kereséshez használható kulcsszavakat.
/// </summary>
public async Task<ProductResolutionHint> 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<ProductResolutionHint>(clean, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return hint ?? new ProductResolutionHint { CanonicalName = rawProductText, SearchKeywords = [rawProductText] };
}
catch
{
return new ProductResolutionHint { CanonicalName = rawProductText, SearchKeywords = [rawProductText] };
}
}
/// <summary>
/// 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.
/// </summary>
public async Task<List<int>> 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<ParsedDraftGroup> Groups { get; set; } = [];
}
public class ParsedDraftGroup
{
[JsonPropertyName("deliveryDate")]
public string DeliveryDate { get; set; } = string.Empty;
[JsonPropertyName("items")]
public List<ParsedDraftItem> 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<string> SearchKeywords { get; set; } = [];
}