using System.Text.Json; using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; namespace Deal.Infrastructure.Integrations.Services; /// /// Локальная реализация без внешнего ML-сервиса. /// /// KV-хранилище настроек тенанта (таблица settings). /// Хранилище обучения ML: очередь MlOutbox + счётчик журнала CardMoves. public sealed class LocalMlClient(ISettingsStore store, IMlLearningStore learningStore) : IMlClient { private static readonly IReadOnlyDictionary EmptyClasses = new Dictionary(); private static readonly IReadOnlyDictionary EmptyScores = new Dictionary(); /// public async Task StatusAsync(CancellationToken ct) { var service = new MlServiceStatusDto( Ready: false, Classes: EmptyClasses, Learned: 0, Eval: new MlEvalDto(Count: 0, Correct: 0, Accuracy: 0.0)); bool enabled = await ReadMlEnabledAsync(ct); int mlDecisions = await ReadCounterAsync(SettingsKeys.MlDecisions, ct); int aiDecisions = await ReadCounterAsync(SettingsKeys.AiDecisions, ct); int learning = await learningStore.CountLearningAsync(ct); int outbox = await learningStore.CountOutboxAsync(ct); var stats = new MlStatsDto( Ml: mlDecisions, Ai: aiDecisions, Learning: learning, Ready: service.Ready, Classes: service.Classes, Learned: service.Learned, Reachable: true, Outbox: outbox); return new MlStatusResponseDto(Enabled: enabled, Service: service, Reachable: true, Stats: stats); } /// public Task PredictAsync(string text, CancellationToken ct) { return Task.FromResult(new MlPredictResultDto( Take: false, Label: null, Scores: EmptyScores, Hits: 0, Ready: false, Margin: null, Terms: Array.Empty(), Type: null)); } /// public async Task ResetAsync(CancellationToken ct) { await learningStore.ClearOutboxAsync(ct); return new MlResetResultDto(Ok: true, Error: null); } /// public async Task PushAsync( string text, string label, double delta, CancellationToken ct) { await MlOutboxQueue.PushAsync(learningStore, text, label, delta, ct); } private async Task ReadMlEnabledAsync(CancellationToken ct) { SettingValue? row = await store.GetAsync(SettingsKeys.MlEnabled, ct); if (row is null) { return SettingsDefaults.MlEnabled; } try { using JsonDocument document = JsonDocument.Parse(row.ValueJson); if (document.RootElement.ValueKind is JsonValueKind.True or JsonValueKind.False) { return document.RootElement.GetBoolean(); } } catch (JsonException) { // Повреждённая строка — дефолт (мягкая семантика, как в SettingsService). } return SettingsDefaults.MlEnabled; } // Читает целочисленный счётчик (mlDecisions/aiDecisions); отсутствие/повреждение → 0. // key: Внутренний KV-ключ счётчика. // ct: Токен отмены. // Возвращает: Значение счётчика из хранилища или 0. private async Task ReadCounterAsync(string key, CancellationToken ct) { SettingValue? row = await store.GetAsync(key, ct); if (row is null) { return 0; } try { using JsonDocument document = JsonDocument.Parse(row.ValueJson); if (document.RootElement.ValueKind == JsonValueKind.Number && document.RootElement.TryGetInt64(out long wide)) { return (int)Math.Clamp(wide, int.MinValue, int.MaxValue); } } catch (JsonException) { // Повреждённая строка — 0 (мягкая семантика, как в SettingsService). } return 0; } }