using InboxIntel.Application.Abstractions; using InboxIntel.Domain.Enums; using InboxIntel.Infrastructure.Configuration; using Microsoft.Extensions.Options; using System.Linq; using System.Net.Http.Json; using System.Text.Json; namespace InboxIntel.Infrastructure.Ai; /// No-op provider used when AI is disabled. Returns empty completions. public class NullAiProvider : IAiProvider { public AiProviderMode Mode => AiProviderMode.Disabled; public Task CompleteAsync(string systemPrompt, string userPrompt, CancellationToken ct = default) => Task.FromResult(string.Empty); } /// No-op embeddings used when AI is disabled. Returns empty vectors so callers fall /// back to lexical search. public class NullEmbeddingProvider : IEmbeddingProvider { public bool IsAvailable => false; public Task EmbedAsync(string text, CancellationToken ct = default) => Task.FromResult(Array.Empty()); public Task> EmbedBatchAsync(IReadOnlyList texts, CancellationToken ct = default) => Task.FromResult>(Array.Empty()); } /// Local embeddings via Ollama's /api/embeddings endpoint (e.g. nomic-embed-text). public class OllamaEmbeddingProvider : IEmbeddingProvider { private readonly HttpClient _http; private readonly AiOptions _options; public OllamaEmbeddingProvider(IHttpClientFactory factory, IOptions options) { _options = options.Value; _http = factory.CreateClient("ollama"); _http.BaseAddress = new Uri(_options.OllamaBaseUrl); } public bool IsAvailable => true; public async Task EmbedAsync(string text, CancellationToken ct = default) { var payload = new { model = _options.EmbeddingModel, prompt = text }; var resp = await _http.PostAsJsonAsync("/api/embeddings", payload, ct); resp.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct)); return doc.RootElement.GetProperty("embedding").EnumerateArray() .Select(e => e.GetSingle()).ToArray(); } // Ollama's /api/embeddings takes one prompt per call, so batch is a sequential loop. // Kept behind the interface so a future batch endpoint is a drop-in swap. public async Task> EmbedBatchAsync(IReadOnlyList texts, CancellationToken ct = default) { var results = new List(texts.Count); foreach (var t in texts) results.Add(await EmbedAsync(t, ct)); return results; } } /// Local LLM via Ollama's /api/chat endpoint. public class OllamaProvider : IAiProvider { private readonly HttpClient _http; private readonly AiOptions _options; public OllamaProvider(IHttpClientFactory factory, IOptions options) { _options = options.Value; _http = factory.CreateClient("ollama"); _http.BaseAddress = new Uri(_options.OllamaBaseUrl); } public AiProviderMode Mode => AiProviderMode.LocalOllama; public async Task CompleteAsync(string systemPrompt, string userPrompt, CancellationToken ct = default) { var payload = new { model = _options.OllamaModel, stream = false, messages = new[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt } } }; var resp = await _http.PostAsJsonAsync("/api/chat", payload, ct); resp.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct)); return doc.RootElement.GetProperty("message").GetProperty("content").GetString() ?? string.Empty; } } /// Cloud LLM via the OpenAI Chat Completions API (optional). public class OpenAiProvider : IAiProvider { private readonly HttpClient _http; private readonly AiOptions _options; public OpenAiProvider(IHttpClientFactory factory, IOptions options) { _options = options.Value; _http = factory.CreateClient("openai"); _http.BaseAddress = new Uri("https://api.openai.com"); _http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _options.OpenAiApiKey); } public AiProviderMode Mode => AiProviderMode.CloudOpenAi; public async Task CompleteAsync(string systemPrompt, string userPrompt, CancellationToken ct = default) { var payload = new { model = _options.OpenAiModel, messages = new[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt } } }; var resp = await _http.PostAsJsonAsync("/v1/chat/completions", payload, ct); resp.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct)); return doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString() ?? string.Empty; } }