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;
}
}