Files
Deal/src/core/Deal.Infrastructure/Integrations/MlOutboxQueue.cs
T
Rustam Khalimov cd0b3b606b Разбить модули Deal.Modules.* по назначению
Application проектов Discovery, Kanban, Pipeline, Settings, Tenants
разделён на Abstractions/Exceptions/Extensions/Models/Registrars/Services;
namespace приведён к путям, using потребителей мигрированы и
дедуплицированы (169 файлов), cref/FQN обновлены.
2026-09-11 13:18:14 +03:00

71 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 L4049) для адаптеров 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;
/// <summary>
/// Пишет строку очереди обучения: trim text/label (пустые — no-op), text[:6000], id mle_+hex.
/// </summary>
/// <param name="learningStore">Хранилище обучения (таблица MlOutbox схемы тенанта).</param>
/// <param name="text">Текст обучающего примера (source_msg карточки или title).</param>
/// <param name="label">Метка: id доски (<c>b_...</c>), <c>spam</c> либо <c>t:hire|t:order</c>.</param>
/// <param name="delta">Вес сигнала (1.0 — учить, −1.0 — снять метку).</param>
/// <param name="ct">Токен отмены.</param>
/// <returns>Задача завершается после записи строки (отправку делает фоновый флашер).</returns>
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;
}
}