| | | 1 | | using System.Text.RegularExpressions; |
| | | 2 | | using OzonDomains; |
| | | 3 | | |
| | | 4 | | namespace Services.AiAssistant; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Определяет write-интенты (редактирование заказа, транзакции) из пользовательских сообщений. |
| | | 8 | | /// </summary> |
| | | 9 | | public static partial class AiWriteIntentParser |
| | | 10 | | { |
| | | 11 | | // ── Общие детекторы ──────────────────────────────────────────────────────── |
| | | 12 | | |
| | | 13 | | public static bool IsEditOrderIntent(string lower) => |
| | 16 | 14 | | (lower.Contains("измен") || lower.Contains("поставь") || lower.Contains("установи") || |
| | 16 | 15 | | lower.Contains("смени") || lower.Contains("обнови") || lower.Contains("поменяй") || |
| | 16 | 16 | | lower.Contains("задай") || lower.Contains("назначь") || lower.Contains("укажи")) && |
| | 16 | 17 | | (lower.Contains("поставщик") || lower.Contains("склад") || lower.Contains("статус") || |
| | 16 | 18 | | lower.Contains("цен") || lower.Contains("количеств") || lower.Contains("номер заказ")); |
| | | 19 | | |
| | | 20 | | public static bool IsTransactionIntent(string lower) |
| | | 21 | | { |
| | 25 | 22 | | if (AiOrderFilterParser.IsReadOnlyOrderSearchIntent(lower)) |
| | 3 | 23 | | return false; |
| | | 24 | | |
| | 22 | 25 | | return lower.Contains("проведи") || lower.Contains("проведите") || lower.Contains("провести") || |
| | 22 | 26 | | lower.Contains("заказан поставщику") || lower.Contains("заказан реализатору") || |
| | 22 | 27 | | lower.Contains("принят на склад") || |
| | 22 | 28 | | lower.Contains("отгружен поставщиком") || lower.Contains("отгрузить клиенту") || |
| | 22 | 29 | | lower.Contains("отгрузить к клиент") || |
| | 22 | 30 | | (lower.Contains("создай транзакци") || lower.Contains("создать транзакци")) || |
| | 22 | 31 | | lower.Contains("к отмене") || lower.Contains("пометь к отмене") || |
| | 22 | 32 | | (lower.Contains("отмен") && (lower.Contains("заказ") || lower.Contains("транзакц")) |
| | 22 | 33 | | && !LooksLikeStatusFilterPhrase(lower)) || |
| | 22 | 34 | | lower.Contains("возврат покупател") || lower.Contains("вернуть поставщику") || |
| | 22 | 35 | | lower.Contains("возврат на склад") || lower.Contains("перемести") || |
| | 22 | 36 | | lower.Contains("изменение склада") || lower.Contains("смени склад"); |
| | | 37 | | } |
| | | 38 | | |
| | | 39 | | /// <summary>«со статусом Отменен» — фильтр поиска, не команда отменить заказ.</summary> |
| | | 40 | | private static bool LooksLikeStatusFilterPhrase(string lower) => |
| | 2 | 41 | | lower.Contains("со статус") || lower.Contains("статусом") || lower.Contains("в статусе") |
| | 2 | 42 | | || lower.Contains("статус "); |
| | | 43 | | |
| | | 44 | | // ── Тип транзакции ───────────────────────────────────────────────────────── |
| | | 45 | | |
| | 1 | 46 | | private static readonly (string[] Keywords, TransactionType Type)[] TransactionKeywordMap = |
| | 1 | 47 | | [ |
| | 1 | 48 | | (["заказан поставщику", "закажи у поставщика", "заказать у поставщика"], TransactionType.OrderedToSupplier), |
| | 1 | 49 | | (["заказан реализатору", "закажи у реализатора", "заказать у реализатора"], TransactionType.OrderedToSeller), |
| | 1 | 50 | | (["отгружен поставщиком", "поставщик отгрузил", "принята от поставщика"], TransactionType.ShippedBySupplier), |
| | 1 | 51 | | (["отгружен реализатором", "реализатор отгрузил"], TransactionType.ShippedToSeller), |
| | 1 | 52 | | (["принят на склад", "принять на склад", "принято на склад"], TransactionType.AcceptedToWarehouse), |
| | 1 | 53 | | (["отгружен клиенту", "отгрузить клиенту", "отправить клиенту"], TransactionType.ShippedToClient), |
| | 1 | 54 | | (["возврат покупателя", "покупатель вернул", "возврат от покупателя"], TransactionType.CustomerReturn), |
| | 1 | 55 | | (["вернуть поставщику", "возвращено поставщику", "возврат поставщику"], TransactionType.ReturnToSupplier), |
| | 1 | 56 | | (["возврат на склад", "вернуть на склад", "вернуть на наш склад"], TransactionType.ReturnToWarehouse), |
| | 1 | 57 | | (["перемещение между складами", "переместить между складами", "перемести товар"], TransactionType.WarehouseTrans |
| | 1 | 58 | | (["изменение склада", "смени склад", "изменить склад"], TransactionType.WarehouseChange), |
| | 1 | 59 | | (["к отмене", "пометь к отмене", "пометить к отмене"], TransactionType.ToCancellation), |
| | 1 | 60 | | (["отменить заказ", "отмена заказа", "провести отмену", "создать транзакцию отмен"], TransactionType.Cancelled), |
| | 1 | 61 | | (["отгружен комиссионеру", "отправить на склад ozon", "отгрузить ozon"], TransactionType.ShippedToCommissioner), |
| | 1 | 62 | | (["утерян реализатором", "потерян реализатором"], TransactionType.LostByCommissioner), |
| | 1 | 63 | | (["перемещение", "перенести заказ"], TransactionType.ReturnTransfer), |
| | 1 | 64 | | ]; |
| | | 65 | | |
| | | 66 | | public static TransactionType? TryParseTransactionType(string lower) |
| | | 67 | | { |
| | 920 | 68 | | foreach (var (keywords, type) in TransactionKeywordMap) |
| | | 69 | | { |
| | 434 | 70 | | if (keywords.Any(lower.Contains)) |
| | 46 | 71 | | return type; |
| | | 72 | | } |
| | 3 | 73 | | return null; |
| | | 74 | | } |
| | | 75 | | |
| | | 76 | | // ── Извлечение имён из сообщения ──────────────────────────────────────────── |
| | | 77 | | |
| | | 78 | | [GeneratedRegex(@"поставщик[аеу]?\s+(?:на\s+)?[«""']?([А-Яа-яёA-Za-z0-9][А-Яа-яёA-Za-z0-9\s\-]{1,40}?)[»""']?(?:\s|$ |
| | | 79 | | private static partial Regex SupplierNameRegex(); |
| | | 80 | | |
| | | 81 | | [GeneratedRegex(@"(?:статус|статуса?)\s+(?:на\s+)?[«""']?([А-Яа-яё][А-Яа-яё\s]{3,40}?)[»""']?(?:\s|$|,|\.)", RegexOp |
| | | 82 | | private static partial Regex StatusNameRegex(); |
| | | 83 | | |
| | | 84 | | [GeneratedRegex(@"(?:склад[еу]?|склад отгрузки)\s+(?:на\s+)?[«""']?([А-Яа-яёA-Za-z0-9][А-Яа-яёA-Za-z0-9\s\-]{1,40}?) |
| | | 85 | | private static partial Regex WarehouseNameRegex(); |
| | | 86 | | |
| | | 87 | | // Склад-отправитель: "со склада X", "отправитель X", "с X" |
| | | 88 | | [GeneratedRegex(@"(?:со?\s+склад[аеу]?|склад[аеу]?\s*-\s*отправитель|откуда\s+склад)\s+[«""']?([А-Яа-яёA-Za-z0-9][А- |
| | | 89 | | private static partial Regex WarehouseSenderRegex(); |
| | | 90 | | |
| | | 91 | | // Склад-получатель: "на склад X", "получатель X", "куда X" |
| | | 92 | | [GeneratedRegex(@"(?:на\s+склад[еу]?|склад[аеу]?\s*-\s*получатель|куда\s+склад)\s+[«""']?([А-Яа-яёA-Za-z0-9][А-Яа-яё |
| | | 93 | | private static partial Regex WarehouseRecipientRegex(); |
| | | 94 | | |
| | | 95 | | // Матчит: "цена 100", "цену 100", "цена закупки 100", "цена на 100", "стоимость 100", "по 100 руб" |
| | | 96 | | [GeneratedRegex(@"(?:цен[аеуы]?\s*(?:закупки)?\s*(?:на\s+)?|стоимость\s+|по\s+)(\d[\d\s,\.]*)", RegexOptions.IgnoreC |
| | | 97 | | private static partial Regex PriceRegex(); |
| | | 98 | | |
| | | 99 | | [GeneratedRegex(@"количеств[оа]\s+(?:на\s+)?(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled)] |
| | | 100 | | private static partial Regex QuantityRegex(); |
| | | 101 | | |
| | | 102 | | public static string? TryExtractSupplierName(string message) |
| | | 103 | | { |
| | 8 | 104 | | var m = SupplierNameRegex().Match(message); |
| | 8 | 105 | | return m.Success ? m.Groups[1].Value.Trim() : null; |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | public static string? TryExtractStatusName(string message) |
| | | 109 | | { |
| | 7 | 110 | | var m = StatusNameRegex().Match(message); |
| | 7 | 111 | | return m.Success ? m.Groups[1].Value.Trim() : null; |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | public static string? TryExtractWarehouseName(string message) |
| | | 115 | | { |
| | 8 | 116 | | var m = WarehouseNameRegex().Match(message); |
| | 8 | 117 | | return m.Success ? m.Groups[1].Value.Trim() : null; |
| | | 118 | | } |
| | | 119 | | |
| | | 120 | | public static string? TryExtractWarehouseSender(string message) |
| | | 121 | | { |
| | 5 | 122 | | var m = WarehouseSenderRegex().Match(message); |
| | 8 | 123 | | if (m.Success) return m.Groups[1].Value.Trim(); |
| | | 124 | | // Fallback: первый склад из generic regex |
| | 2 | 125 | | return TryExtractWarehouseName(message); |
| | | 126 | | } |
| | | 127 | | |
| | | 128 | | public static string? TryExtractWarehouseRecipient(string message) |
| | | 129 | | { |
| | 4 | 130 | | var m = WarehouseRecipientRegex().Match(message); |
| | 4 | 131 | | return m.Success ? m.Groups[1].Value.Trim() : null; |
| | | 132 | | } |
| | | 133 | | |
| | | 134 | | public static decimal? TryExtractPrice(string message) |
| | | 135 | | { |
| | 9 | 136 | | var m = PriceRegex().Match(message); |
| | 12 | 137 | | if (!m.Success) return null; |
| | 6 | 138 | | var raw = m.Groups[1].Value.Replace(" ", "").Replace(",", "."); |
| | 6 | 139 | | return decimal.TryParse(raw, System.Globalization.NumberStyles.Number, |
| | 6 | 140 | | System.Globalization.CultureInfo.InvariantCulture, out var v) ? v : null; |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | public static int? TryExtractQuantity(string message) |
| | | 144 | | { |
| | 5 | 145 | | var m = QuantityRegex().Match(message); |
| | 5 | 146 | | return m.Success && int.TryParse(m.Groups[1].Value, out var v) ? v : null; |
| | | 147 | | } |
| | | 148 | | |
| | | 149 | | // ── Обязательные поля для транзакций ──────────────────────────────────────── |
| | | 150 | | |
| | 1 | 151 | | private static readonly HashSet<TransactionType> RequiresWarehouses = new() |
| | 1 | 152 | | { |
| | 1 | 153 | | TransactionType.CustomerReturn, |
| | 1 | 154 | | TransactionType.ReturnToSupplier, |
| | 1 | 155 | | TransactionType.ReturnToWarehouse, |
| | 1 | 156 | | TransactionType.WarehouseTransfer, |
| | 1 | 157 | | TransactionType.LostByCommissioner, |
| | 1 | 158 | | }; |
| | | 159 | | |
| | 14 | 160 | | public static bool NeedsWarehouses(TransactionType type) => RequiresWarehouses.Contains(type); |
| | | 161 | | |
| | | 162 | | public static List<string> GetMissingTransactionFields( |
| | | 163 | | TransactionType type, |
| | | 164 | | string? warehouseSender, |
| | | 165 | | string? warehouseRecipient) |
| | | 166 | | { |
| | 6 | 167 | | var missing = new List<string>(); |
| | 8 | 168 | | if (!NeedsWarehouses(type)) return missing; |
| | | 169 | | |
| | 4 | 170 | | if (string.IsNullOrWhiteSpace(warehouseSender)) |
| | 2 | 171 | | missing.Add("склад-отправитель"); |
| | 4 | 172 | | if (string.IsNullOrWhiteSpace(warehouseRecipient) && type != TransactionType.WarehouseChange) |
| | 2 | 173 | | missing.Add("склад-получатель"); |
| | 4 | 174 | | return missing; |
| | | 175 | | } |
| | | 176 | | } |