journal/Journal.Core/Services/PythonSidecarAiService.cs
Jacob Schmidt d3781d6c3e refactor: slim Entry.cs, extract shared sidecar client, add entry file repository
- Slim Entry.cs from ~550 to ~330 lines (thin dispatcher only)
- Extract HtmlSanitizer, CommandLogger, EntryFileService from Entry.cs
- Extract PythonSidecarClient from duplicated sidecar plumbing
- Add IEntryFileRepository + DiskEntryFileRepository (mirrors Fragment pattern)
- Move payload records to Dtos/CommandDtos.cs
- Move database result records to Dtos/DatabaseDtos.cs
- Register new services and repository in DI
- All 70/70 smoke tests pass, no behavior changes

Co-Authored-By: Warp <agent@warp.dev>
2026-02-23 21:03:55 -06:00

86 lines
3.7 KiB
C#

using System.Text.Json;
using Journal.Core.Dtos;
using Journal.Core.Models;
namespace Journal.Core.Services;
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<AiHealthDto> 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 JsonValueKind.False ? false : true);
return new AiHealthDto(provider, Enabled: true, Healthy: healthy, Message: message);
}
public async Task<string> 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<string> SummarizeAllAsync(IReadOnlyList<string> entries, CancellationToken cancellationToken = default)
{
entries ??= [];
var data = await _client.SendAsync("summarize_all", new { entries }, cancellationToken);
return data?.GetString() ?? "";
}
public async Task<string> 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<IReadOnlyList<double>> 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<double>();
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;
}
}