| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Text; |
| | | 3 | | using OzonDomains; |
| | | 4 | | |
| | | 5 | | namespace Services.TransactionCommentAi; |
| | | 6 | | |
| | | 7 | | public static class TransactionCommentAiPromptBuilder |
| | | 8 | | { |
| | 1 | 9 | | private static readonly CultureInfo Ru = CultureInfo.GetCultureInfo("ru-RU"); |
| | 1 | 10 | | private static readonly TimeZoneInfo RussianTimeZone = TimeZoneInfo.FindSystemTimeZoneById( |
| | 1 | 11 | | OperatingSystem.IsWindows() ? "Russian Standard Time" : "Europe/Moscow"); |
| | | 12 | | |
| | | 13 | | public const string SystemPrompt = """ |
| | | 14 | | Ты — операционный ассистент Studio2. Пишешь комментарий к транзакции для журнала заказов. |
| | | 15 | | Правила: |
| | | 16 | | - Используй только факты из контекста, ничего не выдумывай. |
| | | 17 | | - Формат: plain text, без HTML, Markdown, служебных рассуждений и обращения к читателю. |
| | | 18 | | - Язык: русский, деловой стиль. |
| | | 19 | | - Точная структура: «Тип», «Когда», «Пользователь», «Что произошло». Не повторяй ID транзакции. |
| | | 20 | | - В «Что произошло» сначала напиши сводку по количеству, статусам, split и отменам; затем перечисли переданные в |
| | | 21 | | - По каждому заказу сохраняй номер отправления, товар (артикул, наименование, qty), клиента, город, поставщика, |
| | | 22 | | - Указывай деньги только если они переданы: закупочную цену и диапазон прибыли. Не вычисляй прибыль и не оценива |
| | | 23 | | - Split-часть прямо обозначай как часть split-заказа; строку «К отмене» или «Отменен» — как отмену/подготовку к |
| | | 24 | | - Для транзакций с 1С явно укажи склады отправителя/получателя, номера документов, факт проведения и все ошибки. |
| | | 25 | | - Для отгрузки или отмены Ozon укажи итоговые счётчики и результаты по отправлениям; ошибки не сглаживай. |
| | | 26 | | - Учитывай смысл типа транзакции: заказ поставщику/реализатору — размещение; отгрузка поставщиком/реализатором/к |
| | | 27 | | - Если факт отсутствует в контексте, не пиши «не указано», не додумывай и не добавляй шаблонных рекомендаций. |
| | | 28 | | - При нескольких заказах: сначала сводка, затем ключевые заказы. Сжимай текст, ориентир — до 4000 символов. |
| | | 29 | | - Не копируй JSON и технические идентификаторы без необходимости. |
| | | 30 | | """; |
| | | 31 | | |
| | | 32 | | public const string UserInstruction = |
| | | 33 | | "Сформируй полный комментарий к транзакции строго по системным правилам. Контекст ниже является единственным ист |
| | | 34 | | |
| | | 35 | | public static string BuildSystemPrompt(TransactionCommentAiContext context) => |
| | 10 | 36 | | SystemPrompt + "\n- Особенность текущей транзакции: " + GetTransactionTypeGuidance(context.TransactionType); |
| | | 37 | | |
| | | 38 | | public static string BuildDataContext(TransactionCommentAiContext context, int maxOrders) |
| | | 39 | | { |
| | 7 | 40 | | var sb = new StringBuilder(); |
| | 7 | 41 | | var performedLocal = TimeZoneInfo.ConvertTimeFromUtc( |
| | 7 | 42 | | DateTime.SpecifyKind(context.PerformedAtUtc, DateTimeKind.Utc), |
| | 7 | 43 | | RussianTimeZone); |
| | | 44 | | |
| | 7 | 45 | | sb.AppendLine($"Тип: {context.TransactionTypeDisplayName} ({context.TransactionType})"); |
| | 7 | 46 | | sb.AppendLine( |
| | 7 | 47 | | $"Когда: {context.PerformedAtUtc:dd.MM.yyyy, HH:mm:ss} (UTC) / {performedLocal:HH:mm} (MSK)"); |
| | 7 | 48 | | sb.AppendLine($"Пользователь: {context.UserName}"); |
| | 7 | 49 | | sb.AppendLine($"ID транзакции: {context.TransactionId}"); |
| | 7 | 50 | | sb.AppendLine(); |
| | | 51 | | |
| | 7 | 52 | | sb.AppendLine("=== Сводка ==="); |
| | 7 | 53 | | sb.AppendLine($"Заказов в транзакции: {context.Batch.TotalOrders}"); |
| | 7 | 54 | | sb.AppendLine($"Отменено: {context.Batch.CancelledCount}"); |
| | 7 | 55 | | sb.AppendLine($"Со split-номером (/1, /2): {context.Batch.SplitCount}"); |
| | 7 | 56 | | sb.AppendLine($"Переведено в «К отмене»/отмену: {context.Batch.ToCancellationCount}"); |
| | 7 | 57 | | if (!string.IsNullOrWhiteSpace(context.Batch.TargetStatusName)) |
| | 5 | 58 | | sb.AppendLine($"Целевой статус: {context.Batch.TargetStatusName}"); |
| | 7 | 59 | | if (context.ProcessIn1C) |
| | 0 | 60 | | sb.AppendLine("Проведение в 1С: да"); |
| | 7 | 61 | | if (context.ShipInOzonRequested) |
| | 0 | 62 | | sb.AppendLine("Отгрузка в Ozon после проведения: запрошена"); |
| | 7 | 63 | | sb.AppendLine(); |
| | | 64 | | |
| | 7 | 65 | | AppendOneC(sb, context.OneC); |
| | 7 | 66 | | AppendOzonShip(sb, context.OzonShip); |
| | 7 | 67 | | AppendOzonCancel(sb, context.OzonCancelNotes); |
| | | 68 | | |
| | 7 | 69 | | var orders = context.Orders; |
| | 7 | 70 | | var shown = orders.Take(Math.Max(1, maxOrders)).ToList(); |
| | 7 | 71 | | var hidden = Math.Max(0, orders.Count - shown.Count); |
| | | 72 | | |
| | 7 | 73 | | sb.AppendLine("=== Заказы ==="); |
| | 28 | 74 | | foreach (var o in shown) |
| | | 75 | | { |
| | 7 | 76 | | sb.AppendLine($"Заказ: {o.ShipmentNumber ?? $"ID {o.OrderId}"}"); |
| | 7 | 77 | | var product = string.Join(" ", new[] |
| | 7 | 78 | | { |
| | 7 | 79 | | o.Article, |
| | 7 | 80 | | !string.IsNullOrWhiteSpace(o.Manufacturer) ? $"({o.Manufacturer})" : null, |
| | 7 | 81 | | !string.IsNullOrWhiteSpace(o.ProductName) ? $"«{o.ProductName}»" : null, |
| | 7 | 82 | | o.Quantity is > 0 ? $"×{o.Quantity}" : null |
| | 35 | 83 | | }.Where(x => !string.IsNullOrWhiteSpace(x))); |
| | | 84 | | |
| | 7 | 85 | | if (!string.IsNullOrWhiteSpace(product)) |
| | 5 | 86 | | sb.AppendLine($" Товар: {product}"); |
| | 7 | 87 | | if (!string.IsNullOrWhiteSpace(o.OzonClientName)) |
| | 0 | 88 | | sb.AppendLine($" Клиент: {o.OzonClientName}"); |
| | 7 | 89 | | if (!string.IsNullOrWhiteSpace(o.DeliveryCity)) |
| | 0 | 90 | | sb.AppendLine($" Город: {o.DeliveryCity}"); |
| | 7 | 91 | | if (!string.IsNullOrWhiteSpace(o.SupplierName)) |
| | 1 | 92 | | sb.AppendLine($" Поставщик: {o.SupplierName}"); |
| | 7 | 93 | | if (!string.IsNullOrWhiteSpace(o.ShipmentWarehouseName)) |
| | 0 | 94 | | sb.AppendLine($" Склад отгрузки: {o.ShipmentWarehouseName}"); |
| | 7 | 95 | | if (!string.IsNullOrWhiteSpace(o.FromStatusName) || !string.IsNullOrWhiteSpace(o.ToStatusName)) |
| | 0 | 96 | | sb.AppendLine($" Статус: {o.FromStatusName ?? "—"} → {o.ToStatusName ?? "—"}"); |
| | 7 | 97 | | if (o.OriginalQuantity is > 0 && o.Quantity != o.OriginalQuantity) |
| | 1 | 98 | | sb.AppendLine($" ⚠ Количество изменено: было {o.OriginalQuantity}, стало {o.Quantity ?? 0}"); |
| | 7 | 99 | | if (o.IsSplitPart) |
| | 0 | 100 | | sb.AppendLine(" ⚠ Часть split-заказа (номер с /1 или /2)"); |
| | 7 | 101 | | if (o.IsToCancellationPart) |
| | 0 | 102 | | sb.AppendLine(" ⚠ Строка ушла в отмену / к отмене"); |
| | 7 | 103 | | if (o.PurchasePrice is > 0) |
| | 0 | 104 | | sb.AppendLine($" Цена закупки: {o.PurchasePrice.Value.ToString("N2", Ru)} ₽"); |
| | 7 | 105 | | if (o.MinProfit is not null || o.MaxProfit is not null) |
| | 0 | 106 | | sb.AppendLine($" Прибыль: {o.MinProfit?.ToString("N2", Ru) ?? "—"} … {o.MaxProfit?.ToString("N2", Ru) ? |
| | 7 | 107 | | if (o.ChangedFields.Count > 0) |
| | 0 | 108 | | sb.AppendLine($" Изменённые поля: {string.Join(", ", o.ChangedFields)}"); |
| | 7 | 109 | | sb.AppendLine(); |
| | | 110 | | } |
| | | 111 | | |
| | 7 | 112 | | if (hidden > 0) |
| | 1 | 113 | | sb.AppendLine($"... и ещё {hidden} заказ(ов) с теми же параметрами транзакции."); |
| | | 114 | | |
| | 7 | 115 | | return sb.ToString().Trim(); |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | private static void AppendOneC(StringBuilder sb, TransactionCommentAiOneCSummary? oneC) |
| | | 119 | | { |
| | 7 | 120 | | if (oneC == null) |
| | 6 | 121 | | return; |
| | | 122 | | |
| | 1 | 123 | | sb.AppendLine("=== 1С ==="); |
| | 1 | 124 | | if (!string.IsNullOrWhiteSpace(oneC.WarehouseSender)) |
| | 1 | 125 | | sb.AppendLine($"Склад отправителя: {oneC.WarehouseSender}"); |
| | 1 | 126 | | if (!string.IsNullOrWhiteSpace(oneC.WarehouseRecipient)) |
| | 0 | 127 | | sb.AppendLine($"Склад получателя: {oneC.WarehouseRecipient}"); |
| | 4 | 128 | | foreach (var doc in oneC.Documents) |
| | | 129 | | { |
| | 1 | 130 | | var label = !string.IsNullOrWhiteSpace(doc.DocumentNumber) ? doc.DocumentNumber : doc.TransactionId; |
| | 1 | 131 | | sb.AppendLine($"Документ: {label}, проведён: {(doc.IsPosted ? "да" : "нет")}"); |
| | | 132 | | } |
| | 2 | 133 | | foreach (var err in oneC.Errors.Distinct()) |
| | 0 | 134 | | sb.AppendLine($"Ошибка 1С: {err}"); |
| | 2 | 135 | | foreach (var err in oneC.PostingErrors.Distinct()) |
| | 0 | 136 | | sb.AppendLine($"Ошибка проведения: {err}"); |
| | 2 | 137 | | foreach (var m in oneC.MissingNomenclature.Distinct()) |
| | 0 | 138 | | sb.AppendLine($"Без номенклатуры: {m}"); |
| | 1 | 139 | | sb.AppendLine(); |
| | 1 | 140 | | } |
| | | 141 | | |
| | | 142 | | private static void AppendOzonShip(StringBuilder sb, TransactionCommentAiOzonShipSummary? ozon) |
| | | 143 | | { |
| | 7 | 144 | | if (ozon == null || ozon.Entries.Count == 0) |
| | 6 | 145 | | return; |
| | | 146 | | |
| | 1 | 147 | | sb.AppendLine("=== Отгрузка в Ozon ==="); |
| | 1 | 148 | | sb.AppendLine($"Успешно: {ozon.SuccessCount}, ошибок: {ozon.FailedCount}, пропущено: {ozon.SkippedCount}"); |
| | 4 | 149 | | foreach (var e in ozon.Entries.Take(15)) |
| | 1 | 150 | | sb.AppendLine($"- {e.ShipmentNumber}: {e.Message}"); |
| | 1 | 151 | | if (ozon.Entries.Count > 15) |
| | 0 | 152 | | sb.AppendLine($"... и ещё {ozon.Entries.Count - 15}"); |
| | 1 | 153 | | sb.AppendLine(); |
| | 1 | 154 | | } |
| | | 155 | | |
| | | 156 | | private static void AppendOzonCancel(StringBuilder sb, List<string> notes) |
| | | 157 | | { |
| | 7 | 158 | | if (notes.Count == 0) |
| | 7 | 159 | | return; |
| | | 160 | | |
| | 0 | 161 | | sb.AppendLine("=== Отмена в Ozon ==="); |
| | 0 | 162 | | foreach (var n in notes.Take(10)) |
| | 0 | 163 | | sb.AppendLine($"- {n}"); |
| | 0 | 164 | | sb.AppendLine(); |
| | 0 | 165 | | } |
| | | 166 | | |
| | 10 | 167 | | private static string GetTransactionTypeGuidance(TransactionType transactionType) => transactionType switch |
| | 10 | 168 | | { |
| | 5 | 169 | | TransactionType.OrderedToSupplier => "заказ размещён у поставщика; отрази поставщика, цену и переход в «Заказан |
| | 0 | 170 | | TransactionType.OrderedToSeller => "заказ размещён у реализатора; отрази реализатора, цену и переход в «Заказан |
| | 0 | 171 | | TransactionType.ShippedBySupplier => "поставщик отгрузил товар; отрази поставщика, складскую/1С-часть и новый ст |
| | 0 | 172 | | TransactionType.ShippedToSeller => "товар отгружен реализатором; отрази движение, результат 1С и новый статус.", |
| | 0 | 173 | | TransactionType.AcceptedToWarehouse => "товар принят на склад; отрази оба склада и документы 1С, если они переда |
| | 0 | 174 | | TransactionType.ShippedToClient => "заказ передан клиенту; отрази отгрузку Ozon и финальный статус только по пер |
| | 0 | 175 | | TransactionType.ShippedToCommissioner => "товар отправлен комиссионеру/Ozon FBO; отрази складское движение и рез |
| | 1 | 176 | | TransactionType.ToCancellation => "заказ только помечен к отмене, не называй его отменённым без соответствующего |
| | 0 | 177 | | TransactionType.Cancelled => "заказ отменён; отрази отмену и связанные с ней части split-заказа или результаты O |
| | 0 | 178 | | TransactionType.CustomerReturn => "это возврат от покупателя; отрази обратное движение товара и склады.", |
| | 0 | 179 | | TransactionType.ReturnToSupplier => "товар возвращён поставщику; отрази поставщика, склады и документы 1С.", |
| | 0 | 180 | | TransactionType.ReturnToWarehouse => "товар возвращён на склад Studio2; отрази склад-получатель и документы 1С." |
| | 1 | 181 | | TransactionType.WarehouseTransfer => "это внутреннее перемещение между складами; обязательно различай отправител |
| | 0 | 182 | | TransactionType.WarehouseChange => "изменён склад отгрузки заказа; не описывай это как физическое перемещение бе |
| | 0 | 183 | | TransactionType.ReturnTransfer => "это внутреннее перемещение после возврата; отрази только фактически переданны |
| | 1 | 184 | | TransactionType.LostByCommissioner => "товар утрачен реализатором; явно отрази исключительную ситуацию и не назы |
| | 1 | 185 | | TransactionType.Percentage => "изменены результаты проценки; перечисли только переданные изменённые поля и финан |
| | 1 | 186 | | TransactionType.ReceiptNote => "создана приходная накладная; отрази документы 1С, проведение и ошибки.", |
| | 0 | 187 | | _ => "опиши операцию только по типу и фактам из контекста." |
| | 10 | 188 | | }; |
| | | 189 | | } |