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