diff --git a/src/Memorizer.UnitTests/EmbeddingApiClientTests.cs b/src/Memorizer.UnitTests/EmbeddingApiClientTests.cs new file mode 100644 index 0000000..2e2f819 --- /dev/null +++ b/src/Memorizer.UnitTests/EmbeddingApiClientTests.cs @@ -0,0 +1,189 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Memorizer.Models; +using Memorizer.Services; +using Memorizer.Settings; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Memorizer.UnitTests; + +public class EmbeddingApiClientTests +{ + [Fact] + public async Task GenerateAsync_OllamaProvider_PostsToApiEmbeddings_WithModelAndPrompt() + { + var handler = new RecordingHandler(JsonSerializer.Serialize(new + { + embedding = new float[] { 0.1f, 0.2f, 0.3f } + })); + + var client = CreateClient(handler, new EmbeddingSettings + { + Provider = ProviderNames.Ollama, + ApiUrl = new Uri("http://embed.local"), + Model = "all-minilm" + }); + + var result = await client.GenerateAsync("all-minilm", "hello world"); + + Assert.Equal([0.1f, 0.2f, 0.3f], result); + Assert.NotNull(handler.LastRequest); + Assert.Equal("/api/embeddings", handler.LastRequest!.RequestUri!.AbsolutePath); + + var body = JsonDocument.Parse(handler.LastRequestBody!); + Assert.Equal("all-minilm", body.RootElement.GetProperty("model").GetString()); + Assert.Equal("hello world", body.RootElement.GetProperty("prompt").GetString()); + Assert.False(body.RootElement.TryGetProperty("input", out _)); + Assert.Null(handler.LastRequest.Headers.Authorization); + } + + [Fact] + public async Task GenerateAsync_OpenAIProvider_PostsToV1Embeddings_WithModelAndInput_AndBearerAuth() + { + var handler = new RecordingHandler(JsonSerializer.Serialize(new + { + data = new[] + { + new { embedding = new float[] { 0.4f, 0.5f, 0.6f }, index = 0 } + } + })); + + var client = CreateClient(handler, new EmbeddingSettings + { + Provider = ProviderNames.OpenAI, + ApiUrl = new Uri("https://api.openai.com"), + Model = "text-embedding-3-small", + ApiKey = "sk-test-key" + }); + + var result = await client.GenerateAsync("text-embedding-3-small", "hello world"); + + Assert.Equal([0.4f, 0.5f, 0.6f], result); + Assert.NotNull(handler.LastRequest); + Assert.Equal("/v1/embeddings", handler.LastRequest!.RequestUri!.AbsolutePath); + + var body = JsonDocument.Parse(handler.LastRequestBody!); + Assert.Equal("text-embedding-3-small", body.RootElement.GetProperty("model").GetString()); + Assert.Equal("hello world", body.RootElement.GetProperty("input").GetString()); + Assert.False(body.RootElement.TryGetProperty("prompt", out _)); + + Assert.NotNull(handler.LastRequest.Headers.Authorization); + Assert.Equal("Bearer", handler.LastRequest.Headers.Authorization!.Scheme); + Assert.Equal("sk-test-key", handler.LastRequest.Headers.Authorization.Parameter); + } + + [Fact] + public async Task GenerateAsync_OpenAIProvider_WithoutApiKey_DoesNotSendAuthHeader() + { + var handler = new RecordingHandler(JsonSerializer.Serialize(new + { + data = new[] { new { embedding = new float[] { 1f }, index = 0 } } + })); + + var client = CreateClient(handler, new EmbeddingSettings + { + Provider = ProviderNames.OpenAI, + ApiUrl = new Uri("http://localhost:8000"), + Model = "local-model", + ApiKey = null + }); + + await client.GenerateAsync("local-model", "probe"); + + Assert.Null(handler.LastRequest!.Headers.Authorization); + } + + [Fact] + public async Task GenerateAsync_ProviderMatchIsCaseInsensitive() + { + var handler = new RecordingHandler(JsonSerializer.Serialize(new + { + data = new[] { new { embedding = new float[] { 1f }, index = 0 } } + })); + + var client = CreateClient(handler, new EmbeddingSettings + { + Provider = "OpenAI", + ApiUrl = new Uri("https://api.openai.com"), + Model = "text-embedding-3-small" + }); + + await client.GenerateAsync("text-embedding-3-small", "x"); + + Assert.Equal("/v1/embeddings", handler.LastRequest!.RequestUri!.AbsolutePath); + } + + [Fact] + public async Task GenerateAsync_OllamaProvider_EmptyResponse_Throws() + { + var handler = new RecordingHandler(JsonSerializer.Serialize(new { embedding = Array.Empty() })); + + var client = CreateClient(handler, new EmbeddingSettings + { + Provider = ProviderNames.Ollama, + ApiUrl = new Uri("http://embed.local"), + Model = "m" + }); + + await Assert.ThrowsAsync(() => client.GenerateAsync("m", "t")); + } + + [Fact] + public async Task GenerateAsync_OpenAIProvider_NoData_Throws() + { + var handler = new RecordingHandler(JsonSerializer.Serialize(new { data = Array.Empty() })); + + var client = CreateClient(handler, new EmbeddingSettings + { + Provider = ProviderNames.OpenAI, + ApiUrl = new Uri("https://api.openai.com"), + Model = "m" + }); + + await Assert.ThrowsAsync(() => client.GenerateAsync("m", "t")); + } + + private static EmbeddingApiClient CreateClient(HttpMessageHandler handler, EmbeddingSettings settings) + { + var httpClient = new HttpClient(handler); + var snapshot = new TestOptionsSnapshot(settings); + return new EmbeddingApiClient(httpClient, snapshot, NullLogger.Instance); + } + + private sealed class RecordingHandler : HttpMessageHandler + { + private readonly string _responseBody; + + public RecordingHandler(string responseBody) + { + _responseBody = responseBody; + } + + public HttpRequestMessage? LastRequest { get; private set; } + public string? LastRequestBody { get; private set; } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + LastRequest = request; + LastRequestBody = request.Content is null + ? null + : await request.Content.ReadAsStringAsync(cancellationToken); + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(_responseBody, Encoding.UTF8, "application/json") + }; + } + } + + private sealed class TestOptionsSnapshot : IOptionsSnapshot where T : class + { + public TestOptionsSnapshot(T value) { Value = value; } + public T Value { get; } + public T Get(string? name) => Value; + } +} diff --git a/src/Memorizer/Controllers/ConfigurationController.cs b/src/Memorizer/Controllers/ConfigurationController.cs index e0e3515..6193b33 100644 --- a/src/Memorizer/Controllers/ConfigurationController.cs +++ b/src/Memorizer/Controllers/ConfigurationController.cs @@ -42,7 +42,10 @@ public ActionResult GetConfiguration() { EmbeddingSettings = new EmbeddingConfigDto { + Provider = EmbeddingSettingsValue.Provider, + ApiUrl = EmbeddingSettingsValue.ApiUrl?.ToString(), ApiConfigured = EmbeddingSettingsValue.ApiUrl != null, + ApiKeyConfigured = !string.IsNullOrWhiteSpace(EmbeddingSettingsValue.ApiKey), Model = EmbeddingSettingsValue.Model, Timeout = EmbeddingSettingsValue.Timeout.ToString() }, @@ -80,7 +83,10 @@ public class SystemConfiguration public class EmbeddingConfigDto { + public string Provider { get; set; } = string.Empty; + public string? ApiUrl { get; set; } public bool ApiConfigured { get; set; } + public bool ApiKeyConfigured { get; set; } public string Model { get; set; } = string.Empty; public string Timeout { get; set; } = string.Empty; } diff --git a/src/Memorizer/Extensions/ServiceCollectionExtensions.cs b/src/Memorizer/Extensions/ServiceCollectionExtensions.cs index 9f210c2..73d1e30 100644 --- a/src/Memorizer/Extensions/ServiceCollectionExtensions.cs +++ b/src/Memorizer/Extensions/ServiceCollectionExtensions.cs @@ -38,12 +38,13 @@ public static IServiceCollection AddEmbeddings( .BindConfiguration("Embeddings") .ValidateOnStart(); - // Register EmbeddingService as Scoped so it gets fresh settings on each request - // Note: HttpClient is configured with base address in the service constructor - services.AddHttpClient(); + // Single typed HttpClient that handles provider-specific request/response shapes + // (Ollama and OpenAI-compatible). EmbeddingService and EmbeddingDimensionService + // consume it instead of holding their own HttpClient. + services.AddHttpClient(); - // Register EmbeddingDimensionService with its own HttpClient (singleton is fine here) - services.AddHttpClient(); + services.AddScoped(); + services.AddScoped(); // Register dimension mismatch state holder for UI warnings services.AddSingleton(); diff --git a/src/Memorizer/Models/OpenAIEmbeddingRequest.cs b/src/Memorizer/Models/OpenAIEmbeddingRequest.cs new file mode 100644 index 0000000..d5cb9e3 --- /dev/null +++ b/src/Memorizer/Models/OpenAIEmbeddingRequest.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Memorizer.Models; + +public class OpenAIEmbeddingRequest +{ + [JsonPropertyName("model")] + public string Model { get; init; } = string.Empty; + + [JsonPropertyName("input")] + public string Input { get; init; } = string.Empty; +} + +public class OpenAIEmbeddingResponse +{ + [JsonPropertyName("data")] + public OpenAIEmbeddingData[] Data { get; init; } = []; +} + +public class OpenAIEmbeddingData +{ + [JsonPropertyName("embedding")] + public float[] Embedding { get; init; } = []; + + [JsonPropertyName("index")] + public int Index { get; init; } +} diff --git a/src/Memorizer/Services/EmbeddingApiClient.cs b/src/Memorizer/Services/EmbeddingApiClient.cs new file mode 100644 index 0000000..cda840d --- /dev/null +++ b/src/Memorizer/Services/EmbeddingApiClient.cs @@ -0,0 +1,92 @@ +using System.Net.Http.Headers; +using Memorizer.Models; +using Memorizer.Settings; +using Microsoft.Extensions.Options; + +namespace Memorizer.Services; + +/// +/// Sends embedding requests to the configured provider and normalises the response +/// to a float[]. Supports Ollama's native /api/embeddings shape and the +/// OpenAI-compatible /v1/embeddings shape (also exposed by LiteLLM, vLLM, Azure +/// OpenAI, LocalAI, etc.). +/// +public interface IEmbeddingApiClient +{ + Task GenerateAsync(string model, string text, CancellationToken cancellationToken = default); +} + +public sealed class EmbeddingApiClient : IEmbeddingApiClient +{ + private readonly HttpClient _httpClient; + private readonly IOptionsSnapshot _settingsSnapshot; + private readonly ILogger _logger; + + private EmbeddingSettings Settings => _settingsSnapshot.Value; + + public EmbeddingApiClient( + HttpClient httpClient, + IOptionsSnapshot settingsSnapshot, + ILogger logger) + { + _httpClient = httpClient; + _settingsSnapshot = settingsSnapshot; + _logger = logger; + + _httpClient.BaseAddress = Settings.ApiUrl; + _httpClient.Timeout = Settings.Timeout; + + if (!string.IsNullOrWhiteSpace(Settings.ApiKey)) + { + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", Settings.ApiKey); + } + } + + public async Task GenerateAsync(string model, string text, CancellationToken cancellationToken = default) + { + var provider = Settings.Provider; + + if (string.Equals(provider, ProviderNames.OpenAI, StringComparison.OrdinalIgnoreCase)) + { + return await GenerateOpenAIAsync(model, text, cancellationToken); + } + + return await GenerateOllamaAsync(model, text, cancellationToken); + } + + private async Task GenerateOllamaAsync(string model, string text, CancellationToken cancellationToken) + { + var request = new EmbeddingRequest { Model = model, Prompt = text }; + + _logger.LogDebug("Sending Ollama embedding request to {ApiUrl}", Settings.ApiUrl); + var response = await _httpClient.PostAsJsonAsync("api/embeddings", request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + if (result?.Embedding is null || result.Embedding.Length == 0) + { + throw new InvalidOperationException("Empty embedding response from Ollama API"); + } + + return result.Embedding; + } + + private async Task GenerateOpenAIAsync(string model, string text, CancellationToken cancellationToken) + { + var request = new OpenAIEmbeddingRequest { Model = model, Input = text }; + + _logger.LogDebug("Sending OpenAI-compatible embedding request to {ApiUrl}", Settings.ApiUrl); + var response = await _httpClient.PostAsJsonAsync("v1/embeddings", request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + var first = result?.Data.FirstOrDefault(); + if (first?.Embedding is null || first.Embedding.Length == 0) + { + throw new InvalidOperationException("Empty embedding response from OpenAI-compatible API"); + } + + return first.Embedding; + } +} diff --git a/src/Memorizer/Services/EmbeddingDimensionService.cs b/src/Memorizer/Services/EmbeddingDimensionService.cs index 8748580..c08bf32 100644 --- a/src/Memorizer/Services/EmbeddingDimensionService.cs +++ b/src/Memorizer/Services/EmbeddingDimensionService.cs @@ -17,7 +17,7 @@ public class EmbeddingDimensionService : IEmbeddingDimensionService { private readonly NpgsqlDataSource _dataSource; private readonly IOptionsSnapshot _settingsSnapshot; - private readonly HttpClient _httpClient; + private readonly IEmbeddingApiClient _apiClient; private readonly ILogger _logger; // Convenience property to get current settings @@ -30,20 +30,17 @@ public class EmbeddingDimensionService : IEmbeddingDimensionService public EmbeddingDimensionService( NpgsqlDataSource dataSource, IOptionsSnapshot settingsSnapshot, - HttpClient httpClient, + IEmbeddingApiClient apiClient, ILogger logger) { _dataSource = dataSource; _settingsSnapshot = settingsSnapshot; - _httpClient = httpClient; - _httpClient.BaseAddress = Settings.ApiUrl; - _httpClient.Timeout = Settings.Timeout; + _apiClient = apiClient; _logger = logger; } public async Task ProbeModelDimensionsAsync(CancellationToken ct = default) { - // Return cached value if we've already probed this model if (_cachedModelDimensions.HasValue && _cachedModelName == Settings.Model) { return _cachedModelDimensions; @@ -53,24 +50,15 @@ public EmbeddingDimensionService( { _logger.LogDebug("Probing embedding model {Model} for dimensions", Settings.Model); - var request = new EmbeddingRequest - { - Model = Settings.Model, - Prompt = "dimension probe" - }; - - var response = await _httpClient.PostAsJsonAsync("api/embeddings", request, ct); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + var embedding = await _apiClient.GenerateAsync(Settings.Model, "dimension probe", ct); - if (result?.Embedding == null || result.Embedding.Length == 0) + if (embedding.Length == 0) { _logger.LogWarning("Empty embedding response from model probe"); return null; } - _cachedModelDimensions = result.Embedding.Length; + _cachedModelDimensions = embedding.Length; _cachedModelName = Settings.Model; _logger.LogInformation("Detected {Dimensions} dimensions from model {Model}", diff --git a/src/Memorizer/Services/EmbeddingService.cs b/src/Memorizer/Services/EmbeddingService.cs index 843ab25..87670b3 100644 --- a/src/Memorizer/Services/EmbeddingService.cs +++ b/src/Memorizer/Services/EmbeddingService.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Memorizer.Models; using Memorizer.Settings; using Microsoft.Extensions.Options; @@ -24,28 +23,23 @@ Task Generate( /// public class EmbeddingService : IEmbeddingService { - private readonly HttpClient _httpClient; + private readonly IEmbeddingApiClient _apiClient; private readonly IOptionsSnapshot _settingsSnapshot; private readonly IEmbeddingDimensionService _dimensionService; private readonly ILogger _logger; - // Convenience property to get current settings value private EmbeddingSettings Settings => _settingsSnapshot.Value; public EmbeddingService( - HttpClient httpClient, + IEmbeddingApiClient apiClient, IOptionsSnapshot settingsSnapshot, IEmbeddingDimensionService dimensionService, ILogger logger) { - _httpClient = httpClient; + _apiClient = apiClient; _settingsSnapshot = settingsSnapshot; _dimensionService = dimensionService; _logger = logger; - - // Configure HttpClient with current settings - _httpClient.BaseAddress = Settings.ApiUrl; - _httpClient.Timeout = Settings.Timeout; } public async Task Generate( @@ -57,35 +51,16 @@ public async Task Generate( { _logger.LogDebug("Generating embedding for text of length {TextLength}", text.Length); - EmbeddingRequest request = new() { Model = Settings.Model, Prompt = text }; - - _logger.LogDebug("Sending request to embedding API at {ApiUrl}", Settings.ApiUrl); - HttpResponseMessage response = await _httpClient.PostAsJsonAsync( - "api/embeddings", - request, - cancellationToken - ); - response.EnsureSuccessStatusCode(); - - EmbeddingResponse? result = - await response.Content.ReadFromJsonAsync( - cancellationToken: cancellationToken - ); - if (result?.Embedding == null || result.Embedding.Length == 0) - { - throw new Exception("Failed to generate embedding: Empty response from API"); - } + var embedding = await _apiClient.GenerateAsync(Settings.Model, text, cancellationToken); - _logger.LogDebug("Successfully generated embedding with {Dimensions} dimensions", result.Embedding.Length); + _logger.LogDebug("Successfully generated embedding with {Dimensions} dimensions", embedding.Length); - return result.Embedding; + return embedding; } catch (Exception ex) { _logger.LogError(ex, "Error generating embedding: {ErrorMessage}", ex.Message); - // Fallback to a random embedding in case of error - // Use effective dimensions from the dimension service (detected > stored > schema > 384) _logger.LogWarning("Falling back to random embedding generation"); var dimensions = await _dimensionService.GetEffectiveDimensionsAsync(cancellationToken); @@ -96,7 +71,6 @@ await response.Content.ReadFromJsonAsync( embedding[i] = (float)random.NextDouble(); } - // Normalize the embedding float sum = 0; for (int i = 0; i < embedding.Length; i++) { diff --git a/src/Memorizer/Services/InitializationService.cs b/src/Memorizer/Services/InitializationService.cs index e01577a..ba79fab 100644 --- a/src/Memorizer/Services/InitializationService.cs +++ b/src/Memorizer/Services/InitializationService.cs @@ -136,6 +136,10 @@ private async Task LoadDatabaseSettingsIntoConfigurationAsync(IStorage storage, { var providerConfig = embeddingProvider.Config.RootElement; + if (!string.IsNullOrWhiteSpace(embeddingProvider.ProviderName)) + { + config["Embeddings:Provider"] = embeddingProvider.ProviderName; + } if (providerConfig.TryGetProperty("apiUrl", out var apiUrlProp)) { config["Embeddings:ApiUrl"] = apiUrlProp.GetString(); @@ -144,10 +148,14 @@ private async Task LoadDatabaseSettingsIntoConfigurationAsync(IStorage storage, { config["Embeddings:Model"] = modelProp.GetString(); } + if (providerConfig.TryGetProperty("apiKey", out var apiKeyProp)) + { + config["Embeddings:ApiKey"] = apiKeyProp.GetString(); + } _logger.LogInformation( - "Loaded embedding settings from database: ApiUrl={ApiUrl}, Model={Model}", - config["Embeddings:ApiUrl"], config["Embeddings:Model"]); + "Loaded embedding settings from database: Provider={Provider}, ApiUrl={ApiUrl}, Model={Model}", + config["Embeddings:Provider"], config["Embeddings:ApiUrl"], config["Embeddings:Model"]); } // Load LLM/Agent provider settings from database @@ -189,23 +197,30 @@ private async Task SeedOrUpdateEmbeddingProviderAsync(IStorage storage, Embeddin var configApiUrl = settings.ApiUrl.ToString().TrimEnd('/'); var configModel = settings.Model; + var configProvider = string.IsNullOrWhiteSpace(settings.Provider) ? ProviderNames.Ollama : settings.Provider; + var configApiKey = settings.ApiKey; + var configDisplayName = string.Equals(configProvider, ProviderNames.OpenAI, StringComparison.OrdinalIgnoreCase) + ? "OpenAI Embeddings" + : "Ollama Embeddings"; + if (activeProvider == null) { // No provider exists - create one from config _logger.LogInformation( - "No embedding provider configured - seeding from environment: ApiUrl={ApiUrl}, Model={Model}", - configApiUrl, configModel); + "No embedding provider configured - seeding from environment: Provider={Provider}, ApiUrl={ApiUrl}, Model={Model}", + configProvider, configApiUrl, configModel); var newProvider = new ProviderSettings { Id = (ProviderSettingsId)Guid.NewGuid(), ProviderType = ProviderTypes.Embedding, - ProviderName = ProviderNames.Ollama, - DisplayName = "Ollama Embeddings", + ProviderName = configProvider, + DisplayName = configDisplayName, Config = JsonDocument.Parse(JsonSerializer.Serialize(new { apiUrl = configApiUrl, - model = configModel + model = configModel, + apiKey = configApiKey })), IsActive = true, CreatedAt = DateTime.UtcNow, @@ -241,12 +256,13 @@ private async Task SeedOrUpdateEmbeddingProviderAsync(IStorage storage, Embeddin { Id = activeProvider.Id, ProviderType = ProviderTypes.Embedding, - ProviderName = activeProvider.ProviderName, - DisplayName = activeProvider.DisplayName, + ProviderName = configProvider, + DisplayName = configDisplayName, Config = JsonDocument.Parse(JsonSerializer.Serialize(new { apiUrl = configApiUrl, - model = configModel + model = configModel, + apiKey = configApiKey })), IsActive = true, CreatedAt = activeProvider.CreatedAt, diff --git a/src/Memorizer/Settings/EmbeddingSettings.cs b/src/Memorizer/Settings/EmbeddingSettings.cs index 4bbc778..ccbec10 100644 --- a/src/Memorizer/Settings/EmbeddingSettings.cs +++ b/src/Memorizer/Settings/EmbeddingSettings.cs @@ -1,3 +1,5 @@ +using Memorizer.Models; + namespace Memorizer.Settings; /// @@ -14,7 +16,20 @@ namespace Memorizer.Settings; /// public class EmbeddingSettings { + /// + /// Provider identifier. Supported values: , . + /// OpenAI selects the OpenAI-compatible POST /v1/embeddings shape, which is also exposed by + /// LiteLLM, vLLM, Azure OpenAI, LocalAI, and similar gateways. + /// + public string Provider { get; set; } = ProviderNames.Ollama; + public Uri ApiUrl { get; set; } = new("http://localhost:11434"); public string Model { get; set; } = "all-minilm"; public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); -} \ No newline at end of file + + /// + /// Optional bearer token sent as Authorization: Bearer <ApiKey>. + /// Required for the hosted OpenAI API; usually omitted for self-hosted compatible servers. + /// + public string? ApiKey { get; set; } +} diff --git a/src/Memorizer/Views/Home/Config.cshtml b/src/Memorizer/Views/Home/Config.cshtml index 0804581..45c01c7 100644 --- a/src/Memorizer/Views/Home/Config.cshtml +++ b/src/Memorizer/Views/Home/Config.cshtml @@ -43,17 +43,29 @@
- - + + - - + + + + + +
API Configured:Provider:
Model:API URL:
API Configured:
+ + + + + + + + @@ -210,7 +222,10 @@ function displayConfiguration(config) { document.getElementById('configContent').style.display = 'block'; // Embedding Configuration + setText('embeddingProvider', config.embeddingSettings.provider); + setText('embeddingApiUrl', config.embeddingSettings.apiUrl); setBadge('embeddingApiConfigured', config.embeddingSettings.apiConfigured, 'Configured', 'Not Configured'); + setBadge('embeddingApiKeyConfigured', config.embeddingSettings.apiKeyConfigured, 'Configured', 'Not Configured'); setText('embeddingModel', config.embeddingSettings.model); setText('embeddingTimeout', config.embeddingSettings.timeout); diff --git a/src/Memorizer/appsettings.example.json b/src/Memorizer/appsettings.example.json index 7562ba8..7228062 100644 --- a/src/Memorizer/appsettings.example.json +++ b/src/Memorizer/appsettings.example.json @@ -3,9 +3,17 @@ "Storage": "Host=localhost;Port=5432;Database=postgmem;Username=postgres;Password=password" }, "Embeddings": { + "Provider": "ollama", "ApiUrl": "http://localhost:11434", "Model": "all-minilm", - "Timeout": "00:01:00" + "Timeout": "00:01:00", + "ApiKey": null, + "_openai_example": { + "Provider": "openai", + "ApiUrl": "https://api.openai.com", + "Model": "text-embedding-3-small", + "ApiKey": "sk-..." + } }, "LLM": { "ApiUrl": "http://localhost:11434",
Model:
API Key:
Timeout: