using System.Security.Cryptography; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; namespace Deal.Infrastructure.Integrations; // Общая запись обучающего сигнала в очередь MlOutbox (ml_client.push L40–49) для адаптеров IMlClient. // Поведение 1:1 с прототипом и с LocalMlClient.PushAsync этапа 3: пустые после trim text/label — // тихий no-op, text обрезается до 6000 символов (без разрыва суррогатной пары), id — mle_ + // 12 случайных hex (store.uid L48). Обучение идёт ВСЕГДА (выключатель mlEnabled его не трогает) — // и в Local-, и в gRPC-режиме сигнал сначала пишется в outbox, отправку в ml-service делает фоновый // MlOutboxFlushScheduler (Ruling 6: PushAsync ВСЕГДА пишет MlOutbox). internal static class MlOutboxQueue { // Максимальная длина текста обучающего примера (ml_client.push L48: text[:6000]). internal const int MaxLearningTextLength = 6000; // Случайный хвост id outbox: 6 байт → 12 hex-символов (прототип store.uid — uuid4().hex[:12]). private const int OutboxIdRandomBytes = 6; /// /// Пишет строку очереди обучения: trim text/label (пустые — no-op), text[:6000], id mle_+hex. /// /// Хранилище обучения (таблица MlOutbox схемы тенанта). /// Текст обучающего примера (source_msg карточки или title). /// Метка: id доски (b_...), spam либо t:hire|t:order. /// Вес сигнала (1.0 — учить, −1.0 — снять метку). /// Токен отмены. /// Задача завершается после записи строки (отправку делает фоновый флашер). public static async Task PushAsync(IMlLearningStore learningStore, string text, string label, double delta, CancellationToken ct) { string trimmedText = (text ?? string.Empty).Trim(); string trimmedLabel = (label ?? string.Empty).Trim(); if (trimmedText.Length == 0 || trimmedLabel.Length == 0) { return; } await learningStore.AddOutboxAsync( NewOutboxId(), TruncateText(trimmedText), trimmedLabel, delta, ct); } // Генерирует id строки outbox: префикс mle_ + 12 случайных hex-символов (прототип store.uid). // Возвращает: Короткий id записи очереди. private static string NewOutboxId() => KanbanIdPrefixes.MlOutbox + Convert.ToHexString(RandomNumberGenerator.GetBytes(OutboxIdRandomBytes)).ToLowerInvariant(); // Обрезает текст до MaxLearningTextLength символов, не разбивая суррогатную пару на конце. // text: Текст (уже trim-нут). // Возвращает: Первые 6000 символов (или весь текст, если короче). // .NET-срез идёт по UTF-16-единицам и может разбить суррогатную пару; Python-срез прототипа // (text[:6000]) режет по code points — хвостовой high-surrogate убираем, чтобы в БД не ушла «битая» пара. private static string TruncateText(string text) { if (text.Length <= MaxLearningTextLength) { return text; } string cut = text[..MaxLearningTextLength]; return char.IsHighSurrogate(cut[^1]) ? cut[..^1] : cut; } }