using System.Text.Json; using Journal.Core.Dtos; using Journal.Core.Models; using Journal.Core.Services.Sidecar; namespace Journal.Core.Services.Ai; public sealed class PythonSidecarAiService : IAiService { private readonly PythonSidecarClient _client; public PythonSidecarAiService(JournalConfig config) { if (string.IsNullOrWhiteSpace(config.PythonAiSidecarPath)) throw new ArgumentException("Python AI sidecar path is required."); if (!File.Exists(config.PythonAiSidecarPath)) throw new FileNotFoundException($"Python AI sidecar not found: {config.PythonAiSidecarPath}"); _client = new PythonSidecarClient(config); } public async Task HealthAsync(CancellationToken cancellationToken = default) { var data = await _client.SendAsync("health", payload: new { }, cancellationToken); if (data is not JsonElement payload || payload.ValueKind != JsonValueKind.Object) return new AiHealthDto("python-sidecar", Enabled: true, Healthy: true, Message: "ok"); var provider = payload.TryGetProperty("provider", out var providerNode) ? providerNode.GetString() ?? "python-sidecar" : "python-sidecar"; var message = payload.TryGetProperty("message", out var messageNode) ? messageNode.GetString() ?? "ok" : "ok"; var healthy = !payload.TryGetProperty("healthy", out var healthyNode) || healthyNode.ValueKind is JsonValueKind.True || (healthyNode.ValueKind is not JsonValueKind.False); return new AiHealthDto(provider, Enabled: true, Healthy: healthy, Message: message); } public async Task SummarizeEntryAsync(string content, string? fileStem = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(content)) throw new ArgumentException("Entry content is required.", nameof(content)); var data = await _client.SendAsync("summarize_entry", new { content, file_stem = fileStem }, cancellationToken); return data?.GetString() ?? ""; } public async Task SummarizeAllAsync(IReadOnlyList entries, CancellationToken cancellationToken = default) { entries ??= []; var data = await _client.SendAsync("summarize_all", new { entries }, cancellationToken); return data?.GetString() ?? ""; } public async Task ChatAsync(string prompt, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(prompt)) throw new ArgumentException("Prompt is required.", nameof(prompt)); var data = await _client.SendAsync("chat", new { prompt }, cancellationToken); return data?.GetString() ?? ""; } public async Task> EmbedAsync(string content, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(content)) throw new ArgumentException("Content is required.", nameof(content)); var data = await _client.SendAsync("embed", new { content }, cancellationToken); if (data is null || data.Value.ValueKind == JsonValueKind.Null) return []; if (data.Value.ValueKind != JsonValueKind.Array) throw new InvalidOperationException("Python AI sidecar embed response must be a numeric array."); var values = new List(); foreach (var item in data.Value.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Number) throw new InvalidOperationException("Python AI sidecar embed response contains a non-numeric value."); values.Add(item.GetDouble()); } return values; } }