231 lines
8.6 KiB
C#
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; } = [];
|
|
}
|