Application проектов Discovery, Kanban, Pipeline, Settings, Tenants разделён на Abstractions/Exceptions/Extensions/Models/Registrars/Services; namespace приведён к путям, using потребителей мигрированы и дедуплицированы (169 файлов), cref/FQN обновлены.
71 lines
4.2 KiB
C#
71 lines
4.2 KiB
C#
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;
|
||
|
||
/// <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;
|
||
}
|
||
}
|