From 790fcb83a05825e1f56ad4caaf344fc4e4cb6f19 Mon Sep 17 00:00:00 2001 From: KrastyTC Date: Thu, 13 Nov 2025 16:46:59 +0200 Subject: [PATCH 01/12] Add Langfuse-integrated prompt provider library Initial implementation of the PromptProvider library with full Langfuse API integration and local prompt fallback. Includes service interfaces, models, options, dependency injection, and README. Supports prompt creation, retrieval, batch operations, and label updates via Langfuse, with local defaults as fallback. --- PromptProvider/DependencyInjection.cs | 23 ++ PromptProvider/Interfaces/ILangfuseService.cs | 51 +++ PromptProvider/Interfaces/IPromptService.cs | 35 ++ PromptProvider/LICENSE | 1 + .../Models/CreateLangfusePromptRequest.cs | 26 ++ .../Models/CreateLangfusePromptResponse.cs | 33 ++ PromptProvider/Models/CreatePromptRequest.cs | 10 + .../Models/GetPromptsBatchRequest.cs | 6 + .../Models/GetPromptsBatchResponse.cs | 7 + .../Models/LangfusePromptListItem.cs | 27 ++ PromptProvider/Models/LangfusePromptModel.cs | 39 +++ .../Models/LangfusePromptsListResponse.cs | 30 ++ PromptProvider/Models/PromptConfiguration.cs | 20 ++ PromptProvider/Models/PromptModel.cs | 21 ++ PromptProvider/Models/PromptResponse.cs | 13 + .../Models/UpdatePromptLabelsRequest.cs | 9 + PromptProvider/Options/LangfuseOptions.cs | 33 ++ PromptProvider/Options/PromptConfiguration.cs | 20 ++ PromptProvider/Options/PromptKeyOptions.cs | 15 + PromptProvider/Options/PromptsOptions.cs | 6 + PromptProvider/PromptProvider.csproj | 15 + PromptProvider/PromptProvider.sln | 18 + PromptProvider/README.md | 3 + PromptProvider/Services/LangfuseService.cs | 319 ++++++++++++++++++ PromptProvider/Services/PromptService.cs | 254 ++++++++++++++ 25 files changed, 1034 insertions(+) create mode 100644 PromptProvider/DependencyInjection.cs create mode 100644 PromptProvider/Interfaces/ILangfuseService.cs create mode 100644 PromptProvider/Interfaces/IPromptService.cs create mode 100644 PromptProvider/LICENSE create mode 100644 PromptProvider/Models/CreateLangfusePromptRequest.cs create mode 100644 PromptProvider/Models/CreateLangfusePromptResponse.cs create mode 100644 PromptProvider/Models/CreatePromptRequest.cs create mode 100644 PromptProvider/Models/GetPromptsBatchRequest.cs create mode 100644 PromptProvider/Models/GetPromptsBatchResponse.cs create mode 100644 PromptProvider/Models/LangfusePromptListItem.cs create mode 100644 PromptProvider/Models/LangfusePromptModel.cs create mode 100644 PromptProvider/Models/LangfusePromptsListResponse.cs create mode 100644 PromptProvider/Models/PromptConfiguration.cs create mode 100644 PromptProvider/Models/PromptModel.cs create mode 100644 PromptProvider/Models/PromptResponse.cs create mode 100644 PromptProvider/Models/UpdatePromptLabelsRequest.cs create mode 100644 PromptProvider/Options/LangfuseOptions.cs create mode 100644 PromptProvider/Options/PromptConfiguration.cs create mode 100644 PromptProvider/Options/PromptKeyOptions.cs create mode 100644 PromptProvider/Options/PromptsOptions.cs create mode 100644 PromptProvider/PromptProvider.csproj create mode 100644 PromptProvider/PromptProvider.sln create mode 100644 PromptProvider/README.md create mode 100644 PromptProvider/Services/LangfuseService.cs create mode 100644 PromptProvider/Services/PromptService.cs diff --git a/PromptProvider/DependencyInjection.cs b/PromptProvider/DependencyInjection.cs new file mode 100644 index 0000000..0254148 --- /dev/null +++ b/PromptProvider/DependencyInjection.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using PromptProvider.Interfaces; +using PromptProvider.Options; +using PromptProvider.Services; + +namespace PromptProvider; + +public static class DependencyInjection +{ + public static IServiceCollection AddPromptProvider( + this IServiceCollection services, + Action? configureLangfuse = null, + Action? configurePrompts = null) + { + if (configureLangfuse is not null) services.Configure(configureLangfuse); + if (configurePrompts is not null) services.Configure(configurePrompts); + + services.AddHttpClient(); + services.AddScoped(); + + return services; + } +} diff --git a/PromptProvider/Interfaces/ILangfuseService.cs b/PromptProvider/Interfaces/ILangfuseService.cs new file mode 100644 index 0000000..8ce8898 --- /dev/null +++ b/PromptProvider/Interfaces/ILangfuseService.cs @@ -0,0 +1,51 @@ +using PromptProvider.Models; + +namespace PromptProvider.Interfaces; + +public interface ILangfuseService +{ + /// + /// Get a prompt by name from Langfuse API. + /// + /// The name of the prompt + /// Optional version of the prompt to retrieve + /// Optional label of the prompt (defaults to "production" if no label or version is set) + /// Cancellation token + /// The Langfuse prompt model + Task GetPromptAsync( + string promptName, + int? version = null, + string? label = null, + CancellationToken cancellationToken = default); + + /// + /// Get all prompts from Langfuse API. + /// + /// Cancellation token + /// List of Langfuse prompt list items + Task> GetAllPromptsAsync(CancellationToken cancellationToken = default); + + /// + /// Create a new version for the prompt with the given name in Langfuse API. + /// + /// The prompt creation request + /// Cancellation token + /// The created prompt response + Task CreatePromptAsync( + CreateLangfusePromptRequest request, + CancellationToken cancellationToken = default); + + /// + /// Update labels for a specific prompt version in Langfuse API. + /// + /// The name of the prompt + /// The version number to update + /// The update request containing new labels + /// Cancellation token + /// The updated prompt model + Task UpdatePromptLabelsAsync( + string promptName, + int version, + UpdatePromptLabelsRequest request, + CancellationToken cancellationToken = default); +} diff --git a/PromptProvider/Interfaces/IPromptService.cs b/PromptProvider/Interfaces/IPromptService.cs new file mode 100644 index 0000000..a01d645 --- /dev/null +++ b/PromptProvider/Interfaces/IPromptService.cs @@ -0,0 +1,35 @@ +using PromptProvider.Models; + +namespace PromptProvider.Interfaces; + +public interface IPromptService +{ + /// + /// Create a new prompt version in Langfuse + /// + Task CreatePromptAsync(CreatePromptRequest request, CancellationToken cancellationToken = default); + + /// + /// Get a prompt by key with optional version or label. Falls back to local defaults if Langfuse is unavailable. + /// + /// The prompt key/name + /// Optional specific version number + /// Optional label (e.g., "production", "latest") + /// Cancellation token + Task GetPromptAsync(string promptKey, int? version = null, string? label = null, CancellationToken cancellationToken = default); + + /// + /// Get all prompts from Langfuse + /// + Task> GetAllPromptsAsync(CancellationToken cancellationToken = default); + + /// + /// Get multiple prompts by keys with optional label. Falls back to local defaults if Langfuse is unavailable. + /// + Task> GetPromptsAsync(IEnumerable promptKeys, string? label = null, CancellationToken cancellationToken = default); + + /// + /// Update labels for a specific prompt version in Langfuse + /// + Task UpdatePromptLabelsAsync(string promptKey, int version, UpdatePromptLabelsRequest request, CancellationToken cancellationToken = default); +} diff --git a/PromptProvider/LICENSE b/PromptProvider/LICENSE new file mode 100644 index 0000000..c198b1f --- /dev/null +++ b/PromptProvider/LICENSE @@ -0,0 +1 @@ +MIT License (c) 2025 \ No newline at end of file diff --git a/PromptProvider/Models/CreateLangfusePromptRequest.cs b/PromptProvider/Models/CreateLangfusePromptRequest.cs new file mode 100644 index 0000000..cd8e6da --- /dev/null +++ b/PromptProvider/Models/CreateLangfusePromptRequest.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace PromptProvider.Models; +public record CreateLangfusePromptRequest +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("commitMessage")] + public string? CommitMessage { get; set; } + + [JsonPropertyName("config")] + public object? Config { get; set; } + + [JsonPropertyName("labels")] + public string[]? Labels { get; set; } + + [JsonPropertyName("tags")] + public string[]? Tags { get; set; } +} diff --git a/PromptProvider/Models/CreateLangfusePromptResponse.cs b/PromptProvider/Models/CreateLangfusePromptResponse.cs new file mode 100644 index 0000000..bc51860 --- /dev/null +++ b/PromptProvider/Models/CreateLangfusePromptResponse.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace PromptProvider.Models; + +public record CreateLangfusePromptResponse +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("version")] + public int Version { get; set; } + + [JsonPropertyName("config")] + public object? Config { get; set; } + + [JsonPropertyName("labels")] + public required string[] Labels { get; set; } + + [JsonPropertyName("tags")] + public required string[] Tags { get; set; } + + [JsonPropertyName("commitMessage")] + public string? CommitMessage { get; set; } + + [JsonPropertyName("resolutionGraph")] + public object? ResolutionGraph { get; set; } +} diff --git a/PromptProvider/Models/CreatePromptRequest.cs b/PromptProvider/Models/CreatePromptRequest.cs new file mode 100644 index 0000000..c0eb961 --- /dev/null +++ b/PromptProvider/Models/CreatePromptRequest.cs @@ -0,0 +1,10 @@ +namespace PromptProvider.Models; + +public record CreatePromptRequest +{ + public required string PromptKey { get; set; } + public required string Content { get; set; } + public string? CommitMessage { get; set; } + public string[]? Labels { get; set; } + public string[]? Tags { get; set; } +} diff --git a/PromptProvider/Models/GetPromptsBatchRequest.cs b/PromptProvider/Models/GetPromptsBatchRequest.cs new file mode 100644 index 0000000..22204e2 --- /dev/null +++ b/PromptProvider/Models/GetPromptsBatchRequest.cs @@ -0,0 +1,6 @@ +namespace PromptProvider.Models; + +public sealed record GetPromptsBatchRequest +{ + public required List Prompts { get; init; } +} diff --git a/PromptProvider/Models/GetPromptsBatchResponse.cs b/PromptProvider/Models/GetPromptsBatchResponse.cs new file mode 100644 index 0000000..eb58caa --- /dev/null +++ b/PromptProvider/Models/GetPromptsBatchResponse.cs @@ -0,0 +1,7 @@ +namespace PromptProvider.Models; + +public sealed record GetPromptsBatchResponse +{ + public required List Prompts { get; init; } + public required List NotFound { get; init; } +} \ No newline at end of file diff --git a/PromptProvider/Models/LangfusePromptListItem.cs b/PromptProvider/Models/LangfusePromptListItem.cs new file mode 100644 index 0000000..a2dbbe3 --- /dev/null +++ b/PromptProvider/Models/LangfusePromptListItem.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace PromptProvider.Models; + +public record LangfusePromptListItem +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("tags")] + public required string[] Tags { get; set; } + + [JsonPropertyName("labels")] + public required string[] Labels { get; set; } + + [JsonPropertyName("lastUpdatedAt")] + public string? LastUpdatedAt { get; set; } + + [JsonPropertyName("versions")] + public int[]? Versions { get; set; } + + [JsonPropertyName("lastConfig")] + public object? LastConfig { get; set; } +} diff --git a/PromptProvider/Models/LangfusePromptModel.cs b/PromptProvider/Models/LangfusePromptModel.cs new file mode 100644 index 0000000..64f7b8b --- /dev/null +++ b/PromptProvider/Models/LangfusePromptModel.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace PromptProvider.Models; + +public record LangfusePromptModel +{ + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + [JsonPropertyName("config")] + public LangfusePromptConfiguration? Config { get; set; } + + [JsonPropertyName("version")] + public int Version { get; set; } + + [JsonPropertyName("labels")] + public required string[] Labels { get; set; } + + [JsonPropertyName("tags")] + public required string[] Tags { get; set; } +} + +public record LangfusePromptConfiguration +{ + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("temperature")] + public double? Temperature { get; set; } + + [JsonPropertyName("supported_languages")] + public string[]? SupportedLanguages { get; set; } +} diff --git a/PromptProvider/Models/LangfusePromptsListResponse.cs b/PromptProvider/Models/LangfusePromptsListResponse.cs new file mode 100644 index 0000000..b6a53a3 --- /dev/null +++ b/PromptProvider/Models/LangfusePromptsListResponse.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace PromptProvider.Models; + +public record LangfusePromptsListResponse +{ + [JsonPropertyName("data")] + public List Data { get; set; } = new(); + + [JsonPropertyName("meta")] + public LangfuseListMeta? Meta { get; set; } + + [JsonPropertyName("pagination")] + public LangfuseListMeta? Pagination { get; set; } +} + +public record LangfuseListMeta +{ + [JsonPropertyName("page")] + public int Page { get; set; } + + [JsonPropertyName("limit")] + public int Limit { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } +} diff --git a/PromptProvider/Models/PromptConfiguration.cs b/PromptProvider/Models/PromptConfiguration.cs new file mode 100644 index 0000000..1a5f63e --- /dev/null +++ b/PromptProvider/Models/PromptConfiguration.cs @@ -0,0 +1,20 @@ +namespace PromptProvider.Models; + +public sealed class PromptConfiguration +{ + /// + /// The prompt key/name + /// + public required string Key { get; set; } + + /// + /// Optional specific version number to use + /// + public int? Version { get; set; } + + /// + /// Optional label to use (e.g., "production", "staging", "latest") + /// If neither version nor label is specified, defaults to "production" + /// + public string? Label { get; set; } +} diff --git a/PromptProvider/Models/PromptModel.cs b/PromptProvider/Models/PromptModel.cs new file mode 100644 index 0000000..237361d --- /dev/null +++ b/PromptProvider/Models/PromptModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PromptProvider.Models; + +[Table("Prompts")] +public record PromptModel +{ + [Key] + public Guid Id { get; set; } + + [Required, MaxLength(120)] + public string PromptKey { get; set; } = null!; + + // Auto-generated immutable timestamp string + [Required, MaxLength(50)] + public string Version { get; init; } = null!; + + [Required] + public string Content { get; set; } = null!; +} \ No newline at end of file diff --git a/PromptProvider/Models/PromptResponse.cs b/PromptProvider/Models/PromptResponse.cs new file mode 100644 index 0000000..9f3d90d --- /dev/null +++ b/PromptProvider/Models/PromptResponse.cs @@ -0,0 +1,13 @@ +namespace PromptProvider.Models; + +public record PromptResponse +{ + public required string PromptKey { get; set; } + public required string Content { get; set; } + public int? Version { get; set; } + public string[]? Labels { get; set; } + public string[]? Tags { get; set; } + public string? Type { get; set; } + public LangfusePromptConfiguration? Config { get; set; } + public string? Source { get; set; } // "Langfuse" or "Local" +} diff --git a/PromptProvider/Models/UpdatePromptLabelsRequest.cs b/PromptProvider/Models/UpdatePromptLabelsRequest.cs new file mode 100644 index 0000000..cfe271e --- /dev/null +++ b/PromptProvider/Models/UpdatePromptLabelsRequest.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace PromptProvider.Models; + +public record UpdatePromptLabelsRequest +{ + [JsonPropertyName("newLabels")] + public required string[] NewLabels { get; set; } +} diff --git a/PromptProvider/Options/LangfuseOptions.cs b/PromptProvider/Options/LangfuseOptions.cs new file mode 100644 index 0000000..d288ccc --- /dev/null +++ b/PromptProvider/Options/LangfuseOptions.cs @@ -0,0 +1,33 @@ +namespace PromptProvider.Options; + +public class LangfuseOptions +{ + private string? _baseUrl; + private string? _publicKey; + private string? _secretKey; + + public string? BaseUrl + { + get => _baseUrl; + set => _baseUrl = value?.Trim(); + } + + public string? PublicKey + { + get => _publicKey; + set => _publicKey = value?.Trim(); + } + + public string? SecretKey + { + get => _secretKey; + set => _secretKey = value?.Trim(); + } + + public bool IsConfigured() + { + return !string.IsNullOrWhiteSpace(BaseUrl) && + !string.IsNullOrWhiteSpace(PublicKey) && + !string.IsNullOrWhiteSpace(SecretKey); + } +} diff --git a/PromptProvider/Options/PromptConfiguration.cs b/PromptProvider/Options/PromptConfiguration.cs new file mode 100644 index 0000000..4e88152 --- /dev/null +++ b/PromptProvider/Options/PromptConfiguration.cs @@ -0,0 +1,20 @@ +namespace PromptProvider.Options; + +public sealed class PromptConfiguration +{ + /// + /// The prompt key/name + /// + public required string Key { get; set; } + + /// + /// Optional specific version number to use + /// + public int? Version { get; set; } + + /// + /// Optional label to use (e.g., "production", "staging", "latest") + /// If neither version nor label is specified, defaults to "production" + /// + public string? Label { get; set; } +} diff --git a/PromptProvider/Options/PromptKeyOptions.cs b/PromptProvider/Options/PromptKeyOptions.cs new file mode 100644 index 0000000..306b87d --- /dev/null +++ b/PromptProvider/Options/PromptKeyOptions.cs @@ -0,0 +1,15 @@ +namespace PromptProvider.Options; + + +public sealed class PromptKeyOptions +{ + public PromptConfiguration? ChatTitlePrompt { get; set; } + public PromptConfiguration? SystemDefault { get; set; } + public PromptConfiguration? FriendlyTone { get; set; } + public PromptConfiguration? DetailedExplanation { get; set; } + public PromptConfiguration? ExplainMistakeSystem { get; set; } + public PromptConfiguration? GlobalChatSystemDefault { get; set; } + public PromptConfiguration? GlobalChatPageContext { get; set; } + public PromptConfiguration? MistakeUserTemplate { get; set; } + public PromptConfiguration? MistakeRuleTemplate { get; set; } +} \ No newline at end of file diff --git a/PromptProvider/Options/PromptsOptions.cs b/PromptProvider/Options/PromptsOptions.cs new file mode 100644 index 0000000..db54878 --- /dev/null +++ b/PromptProvider/Options/PromptsOptions.cs @@ -0,0 +1,6 @@ +namespace PromptProvider.Options; + +public class PromptsOptions +{ + public Dictionary Defaults { get; set; } = new(); +} diff --git a/PromptProvider/PromptProvider.csproj b/PromptProvider/PromptProvider.csproj new file mode 100644 index 0000000..60463b8 --- /dev/null +++ b/PromptProvider/PromptProvider.csproj @@ -0,0 +1,15 @@ + + + net9.0 + enable + enable + + + + + + + + + + diff --git a/PromptProvider/PromptProvider.sln b/PromptProvider/PromptProvider.sln new file mode 100644 index 0000000..9827cce --- /dev/null +++ b/PromptProvider/PromptProvider.sln @@ -0,0 +1,18 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PromptProvider", "PromptProvider.csproj", "{A2B1D580-4D71-4D3A-AF2D-7AE402A3929C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2B1D580-4D71-4D3A-AF2D-7AE402A3929C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2B1D580-4D71-4D3A-AF2D-7AE402A3929C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2B1D580-4D71-4D3A-AF2D-7AE402A3929C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2B1D580-4D71-4D3A-AF2D-7AE402A3929C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/PromptProvider/README.md b/PromptProvider/README.md new file mode 100644 index 0000000..067e2bf --- /dev/null +++ b/PromptProvider/README.md @@ -0,0 +1,3 @@ +# PromptProvider + +Full Langfuse integration with local prompt fallback. \ No newline at end of file diff --git a/PromptProvider/Services/LangfuseService.cs b/PromptProvider/Services/LangfuseService.cs new file mode 100644 index 0000000..a392304 --- /dev/null +++ b/PromptProvider/Services/LangfuseService.cs @@ -0,0 +1,319 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using PromptProvider.Models; +using PromptProvider.Options; +using Microsoft.Extensions.Options; +using PromptProvider.Interfaces; +using Microsoft.Extensions.Logging; + +namespace PromptProvider.Services; + +public class LangfuseService : ILangfuseService +{ + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + private readonly LangfuseOptions _options; + private readonly bool _isConfigured; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public LangfuseService( + ILogger logger, + HttpClient httpClient, + IOptions options) + { + _logger = logger; + _httpClient = httpClient; + _options = options.Value; + _isConfigured = _options.IsConfigured(); + + if (_isConfigured) + { + ConfigureHttpClient(); + } + else + { + _logger.LogWarning("Langfuse is not configured. Service will not be available."); + } + } + + private void ConfigureHttpClient() + { + // Basic authentication with public key as username and secret key as password + var authValue = Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{_options.PublicKey}:{_options.SecretKey}")); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue); + } + + private void ThrowIfNotConfigured() + { + if (!_isConfigured) + { + throw new InvalidOperationException("Langfuse is not configured. Please check your appsettings configuration."); + } + } + + public async Task GetPromptAsync( + string promptName, + int? version = null, + string? label = null, + CancellationToken cancellationToken = default) + { + ThrowIfNotConfigured(); + + if (string.IsNullOrWhiteSpace(promptName)) + { + throw new ArgumentException("Prompt name is required.", nameof(promptName)); + } + + // Default to "production" label if neither version nor label is specified + if (version is null && string.IsNullOrWhiteSpace(label)) + { + label = "production"; + } + + try + { + var queryParams = new List(); + if (version.HasValue) + { + queryParams.Add($"version={version.Value}"); + } + + if (!string.IsNullOrWhiteSpace(label)) + { + queryParams.Add($"label={Uri.EscapeDataString(label)}"); + } + + var queryString = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : string.Empty; + var requestUri = $"/api/public/v2/prompts/{Uri.EscapeDataString(promptName)}{queryString}"; + + _logger.LogInformation("Fetching prompt '{PromptName}' from Langfuse (version: {Version}, label: {Label})", + promptName, version, label); + + var response = await _httpClient.GetAsync(requestUri, cancellationToken); + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _logger.LogWarning("Prompt '{PromptName}' not found in Langfuse", promptName); + return null; + } + + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + var prompt = JsonSerializer.Deserialize(content, JsonOptions); + + _logger.LogInformation("Successfully fetched prompt '{PromptName}' version {Version}", + promptName, prompt?.Version); + + return prompt; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error fetching prompt '{PromptName}' from Langfuse", promptName); + throw; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize prompt '{PromptName}' from Langfuse", promptName); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error fetching prompt '{PromptName}' from Langfuse", promptName); + throw; + } + } + + public async Task> GetAllPromptsAsync(CancellationToken cancellationToken = default) + { + ThrowIfNotConfigured(); + + try + { + _logger.LogInformation("Fetching all prompts from Langfuse"); + + var response = await _httpClient.GetAsync("/api/public/v2/prompts", cancellationToken); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + + // Deserialize the paginated response + var paginatedResponse = JsonSerializer.Deserialize(content, JsonOptions); + if (paginatedResponse?.Data != null) + { + _logger.LogInformation("Successfully fetched {Count} prompts from Langfuse", + paginatedResponse.Data.Count); + return paginatedResponse.Data; + } + + _logger.LogWarning("Received empty or null data from Langfuse prompts API"); + return new List(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error fetching all prompts from Langfuse"); + throw; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize prompts from Langfuse"); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error fetching all prompts from Langfuse"); + throw; + } + } + + public async Task CreatePromptAsync( + CreateLangfusePromptRequest request, + CancellationToken cancellationToken = default) + { + ThrowIfNotConfigured(); + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new ArgumentException("Prompt name is required.", nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.Prompt)) + { + throw new ArgumentException("Prompt content is required.", nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.Type)) + { + throw new ArgumentException("Prompt type is required.", nameof(request)); + } + + try + { + _logger.LogInformation("Creating new prompt version for '{PromptName}' in Langfuse", request.Name); + + var jsonContent = JsonSerializer.Serialize(request, JsonOptions); + var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/public/v2/prompts", httpContent, cancellationToken); + response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + var createdPrompt = JsonSerializer.Deserialize(responseContent, JsonOptions) + ?? throw new InvalidOperationException("Failed to deserialize created prompt response"); + + _logger.LogInformation("Successfully created prompt '{PromptName}' version {Version}", + createdPrompt.Name, createdPrompt.Version); + + return createdPrompt; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error creating prompt '{PromptName}' in Langfuse", request.Name); + throw; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to serialize/deserialize prompt '{PromptName}'", request.Name); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error creating prompt '{PromptName}' in Langfuse", request.Name); + throw; + } + } + + public async Task UpdatePromptLabelsAsync( + string promptName, + int version, + UpdatePromptLabelsRequest request, + CancellationToken cancellationToken = default) + { + ThrowIfNotConfigured(); + + if (string.IsNullOrWhiteSpace(promptName)) + { + throw new ArgumentException("Prompt name is required.", nameof(promptName)); + } + + if (version <= 0) + { + throw new ArgumentException("Version must be greater than 0.", nameof(version)); + } + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (request.NewLabels is null || request.NewLabels.Length == 0) + { + throw new ArgumentException("At least one label is required.", nameof(request)); + } + + try + { + _logger.LogInformation("Updating labels for prompt '{PromptName}' version {Version} in Langfuse", + promptName, version); + + var requestUri = $"/api/public/v2/prompts/{Uri.EscapeDataString(promptName)}/versions/{version}"; + + var jsonContent = JsonSerializer.Serialize(request, JsonOptions); + var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var httpRequest = new HttpRequestMessage(HttpMethod.Patch, requestUri) + { + Content = httpContent + }; + + var response = await _httpClient.SendAsync(httpRequest, cancellationToken); + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _logger.LogWarning("Prompt '{PromptName}' version {Version} not found in Langfuse", + promptName, version); + throw new InvalidOperationException($"Prompt '{promptName}' version {version} not found"); + } + + response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + var updatedPrompt = JsonSerializer.Deserialize(responseContent, JsonOptions) + ?? throw new InvalidOperationException("Failed to deserialize updated prompt response"); + + _logger.LogInformation("Successfully updated labels for prompt '{PromptName}' version {Version}", + updatedPrompt.Name, updatedPrompt.Version); + + return updatedPrompt; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error updating prompt '{PromptName}' version {Version} in Langfuse", + promptName, version); + throw; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to serialize/deserialize prompt '{PromptName}' version {Version}", + promptName, version); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error updating prompt '{PromptName}' version {Version} in Langfuse", + promptName, version); + throw; + } + } +} diff --git a/PromptProvider/Services/PromptService.cs b/PromptProvider/Services/PromptService.cs new file mode 100644 index 0000000..902c8ef --- /dev/null +++ b/PromptProvider/Services/PromptService.cs @@ -0,0 +1,254 @@ +using PromptProvider.Models; +using PromptProvider.Options; +using PromptProvider.Interfaces; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; + +namespace PromptProvider.Services; + +public class PromptService : IPromptService +{ + private readonly ILogger _logger; + private readonly ILangfuseService _langfuseService; + private readonly IOptions _promptsOptions; + private readonly IOptions _langfuseOptions; + + public PromptService( + ILogger logger, + ILangfuseService langfuseService, + IOptions promptsOptions, + IOptions langfuseOptions) + { + _logger = logger; + _langfuseService = langfuseService; + _promptsOptions = promptsOptions; + _langfuseOptions = langfuseOptions; + } + + public async Task CreatePromptAsync(CreatePromptRequest request, CancellationToken cancellationToken = default) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.PromptKey)) + { + throw new ArgumentException("PromptKey is required.", nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.Content)) + { + throw new ArgumentException("Content is required.", nameof(request)); + } + + if (!_langfuseOptions.Value.IsConfigured()) + { + _logger.LogWarning("Cannot create prompt '{PromptKey}' - Langfuse is not configured", request.PromptKey); + throw new InvalidOperationException("Langfuse is not configured. Cannot create prompts."); + } + + try + { + _logger.LogInformation("Creating prompt '{PromptKey}' in Langfuse", request.PromptKey); + + var langfuseRequest = new CreateLangfusePromptRequest + { + Name = request.PromptKey, + Prompt = request.Content, + Type = "text", + CommitMessage = request.CommitMessage, + Labels = request.Labels ?? Array.Empty(), + Tags = request.Tags ?? Array.Empty() + }; + + var created = await _langfuseService.CreatePromptAsync(langfuseRequest, cancellationToken); + + return new PromptResponse + { + PromptKey = created.Name, + Content = created.Prompt, + Version = created.Version, + Labels = created.Labels, + Tags = created.Tags, + Type = created.Type, + Config = created.Config as LangfusePromptConfiguration, + Source = "Langfuse" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create prompt '{PromptKey}' in Langfuse", request.PromptKey); + throw; + } + } + + public async Task GetPromptAsync( + string promptKey, + int? version = null, + string? label = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(promptKey)) + { + throw new ArgumentException("PromptKey is required.", nameof(promptKey)); + } + + // If Langfuse is configured, try to fetch from it first + if (_langfuseOptions.Value.IsConfigured()) + { + try + { + _logger.LogInformation("Fetching prompt '{PromptKey}' (version: {Version}, label: {Label}) from Langfuse", + promptKey, version, label); + + var langfusePrompt = await _langfuseService.GetPromptAsync(promptKey, version, label, cancellationToken); + + if (langfusePrompt != null) + { + _logger.LogInformation("Successfully retrieved prompt '{PromptKey}' from Langfuse", promptKey); + return new PromptResponse + { + PromptKey = langfusePrompt.Name, + Content = langfusePrompt.Prompt, + Version = langfusePrompt.Version, + Labels = langfusePrompt.Labels, + Tags = langfusePrompt.Tags, + Type = langfusePrompt.Type, + Config = langfusePrompt.Config, + Source = "Langfuse" + }; + } + + _logger.LogWarning("Prompt '{PromptKey}' not found in Langfuse, falling back to local defaults", promptKey); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error retrieving prompt '{PromptKey}' from Langfuse, falling back to local defaults", promptKey); + } + } + else + { + _logger.LogInformation("Langfuse is not configured, using local defaults for prompt '{PromptKey}'", promptKey); + } + + // Always fallback to local defaults + return GetPromptFromDefaults(promptKey); + } + + public async Task> GetAllPromptsAsync(CancellationToken cancellationToken = default) + { + if (!_langfuseOptions.Value.IsConfigured()) + { + _logger.LogWarning("Cannot get all prompts - Langfuse is not configured"); + throw new InvalidOperationException("Langfuse is not configured. Cannot retrieve prompts list."); + } + + try + { + _logger.LogInformation("Fetching all prompts from Langfuse"); + return await _langfuseService.GetAllPromptsAsync(cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve all prompts from Langfuse"); + throw; + } + } + + public async Task> GetPromptsAsync( + IEnumerable promptKeys, + string? label = null, + CancellationToken cancellationToken = default) + { + if (promptKeys is null) + { + throw new ArgumentNullException(nameof(promptKeys)); + } + + var keys = promptKeys.Where(k => !string.IsNullOrWhiteSpace(k)).Distinct().ToList(); + if (keys.Count == 0) + { + throw new ArgumentException("At least one prompt key is required.", nameof(promptKeys)); + } + + var results = new List(); + + foreach (var key in keys) + { + var prompt = await GetPromptAsync(key, label: label, cancellationToken: cancellationToken); + if (prompt != null) + { + results.Add(prompt); + } + } + + return results; + } + + public async Task UpdatePromptLabelsAsync( + string promptKey, + int version, + UpdatePromptLabelsRequest request, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(promptKey)) + { + throw new ArgumentException("PromptKey is required.", nameof(promptKey)); + } + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (!_langfuseOptions.Value.IsConfigured()) + { + _logger.LogWarning("Cannot update prompt '{PromptKey}' - Langfuse is not configured", promptKey); + throw new InvalidOperationException("Langfuse is not configured. Cannot update prompts."); + } + + try + { + _logger.LogInformation("Updating labels for prompt '{PromptKey}' version {Version} in Langfuse", + promptKey, version); + + var updated = await _langfuseService.UpdatePromptLabelsAsync(promptKey, version, request, cancellationToken); + + return new PromptResponse + { + PromptKey = updated.Name, + Content = updated.Prompt, + Version = updated.Version, + Labels = updated.Labels, + Tags = updated.Tags, + Type = updated.Type, + Config = updated.Config, + Source = "Langfuse" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update labels for prompt '{PromptKey}' version {Version}", promptKey, version); + throw; + } + } + + private PromptResponse? GetPromptFromDefaults(string promptKey) + { + var defaults = _promptsOptions.Value.Defaults; + if (defaults == null || !defaults.TryGetValue(promptKey, out var content)) + { + _logger.LogWarning("Prompt '{PromptKey}' not found in local defaults either", promptKey); + return null; + } + + _logger.LogInformation("Returning prompt '{PromptKey}' from local defaults", promptKey); + return new PromptResponse + { + PromptKey = promptKey, + Content = content, + Source = "Local" + }; + } +} From 17daea793ef7181fcf76c024fd7eb5769aced061 Mon Sep 17 00:00:00 2001 From: KrastyTC Date: Tue, 18 Nov 2025 16:58:32 +0200 Subject: [PATCH 02/12] Restructure PromptProvider and add default prompts support Moved source files out of PromptProvider subfolder to project root and updated namespaces. Added IDefaultPromptsProvider interface and ConfigurationDefaultPromptsProvider implementation for configurable default prompts. Updated dependency injection to register the default prompts provider. Refactored PromptService to use IDefaultPromptsProvider instead of direct options access. Added CI and NuGet publish GitHub workflows, new README, and initial unit tests. Removed unused files and consolidated project files. --- .github/workflows/ci.yml | 51 ++++++++++ .github/workflows/publish-nuget.yml | 93 +++++++++++++++++++ ...encyInjection.cs => DependencyInjection.cs | 5 +- Interfaces/IDefaultPromptsProvider.cs | 6 ++ .../ILangfuseService.cs | 0 .../IPromptService.cs | 0 LICENSE | 22 +---- .../CreateLangfusePromptRequest.cs | 0 .../CreateLangfusePromptResponse.cs | 0 .../Models => Models}/CreatePromptRequest.cs | 0 .../GetPromptsBatchRequest.cs | 0 .../GetPromptsBatchResponse.cs | 0 .../LangfusePromptListItem.cs | 0 .../Models => Models}/LangfusePromptModel.cs | 0 .../LangfusePromptsListResponse.cs | 0 .../Models => Models}/PromptConfiguration.cs | 0 .../Models => Models}/PromptModel.cs | 0 .../Models => Models}/PromptResponse.cs | 0 .../UpdatePromptLabelsRequest.cs | 0 .../Options => Options}/LangfuseOptions.cs | 0 .../PromptConfiguration.cs | 0 .../Options => Options}/PromptsOptions.cs | 0 PromptProvider.csproj | 29 ++++++ .../PromptProvider.sln => PromptProvider.sln | 0 PromptProvider/LICENSE | 1 - PromptProvider/Options/PromptKeyOptions.cs | 15 --- PromptProvider/PromptProvider.csproj | 15 --- PromptProvider/README.md | 3 - README.md | 34 +++++++ .../ConfigurationDefaultPromptsProvider.cs | 20 ++++ .../Services => Services}/LangfuseService.cs | 0 .../Services => Services}/PromptService.cs | 15 ++- .../PromptProvider.Tests.csproj | 18 ++++ .../PromptServiceTests.cs | 73 +++++++++++++++ 34 files changed, 336 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-nuget.yml rename PromptProvider/DependencyInjection.cs => DependencyInjection.cs (71%) create mode 100644 Interfaces/IDefaultPromptsProvider.cs rename {PromptProvider/Interfaces => Interfaces}/ILangfuseService.cs (100%) rename {PromptProvider/Interfaces => Interfaces}/IPromptService.cs (100%) rename {PromptProvider/Models => Models}/CreateLangfusePromptRequest.cs (100%) rename {PromptProvider/Models => Models}/CreateLangfusePromptResponse.cs (100%) rename {PromptProvider/Models => Models}/CreatePromptRequest.cs (100%) rename {PromptProvider/Models => Models}/GetPromptsBatchRequest.cs (100%) rename {PromptProvider/Models => Models}/GetPromptsBatchResponse.cs (100%) rename {PromptProvider/Models => Models}/LangfusePromptListItem.cs (100%) rename {PromptProvider/Models => Models}/LangfusePromptModel.cs (100%) rename {PromptProvider/Models => Models}/LangfusePromptsListResponse.cs (100%) rename {PromptProvider/Models => Models}/PromptConfiguration.cs (100%) rename {PromptProvider/Models => Models}/PromptModel.cs (100%) rename {PromptProvider/Models => Models}/PromptResponse.cs (100%) rename {PromptProvider/Models => Models}/UpdatePromptLabelsRequest.cs (100%) rename {PromptProvider/Options => Options}/LangfuseOptions.cs (100%) rename {PromptProvider/Options => Options}/PromptConfiguration.cs (100%) rename {PromptProvider/Options => Options}/PromptsOptions.cs (100%) create mode 100644 PromptProvider.csproj rename PromptProvider/PromptProvider.sln => PromptProvider.sln (100%) delete mode 100644 PromptProvider/LICENSE delete mode 100644 PromptProvider/Options/PromptKeyOptions.cs delete mode 100644 PromptProvider/PromptProvider.csproj delete mode 100644 PromptProvider/README.md create mode 100644 README.md create mode 100644 Services/ConfigurationDefaultPromptsProvider.cs rename {PromptProvider/Services => Services}/LangfuseService.cs (100%) rename {PromptProvider/Services => Services}/PromptService.cs (95%) create mode 100644 tests/PromptProvider.Tests/PromptProvider.Tests.csproj create mode 100644 tests/PromptProvider.Tests/PromptServiceTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..83dc2f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: {} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test --no-build --configuration Release + - name: Pack + run: dotnet pack PromptProvider/PromptProvider.csproj -c Release -o ./artifacts + + publish: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + - name: Restore + run: dotnet restore + - name: Pack + run: dotnet pack PromptProvider/PromptProvider.csproj -c Release -o ./artifacts + - name: Publish to NuGet + uses: NuGet/setup-nuget@v2 + - name: Push package + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + for pkg in ./artifacts/*.nupkg; do + dotnet nuget push "$pkg" -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json + done diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml new file mode 100644 index 0000000..9526b26 --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,93 @@ +name: publish-nuget + +on: + push: + branches: [ main ] + workflow_dispatch: + inputs: + version: + description: 'Override version (optional, e.g. 0.1.2)' + required: false + type: string + +jobs: + pack-and-push: + runs-on: ubuntu-latest + + env: + LIB_PROJ: PromptProvider/PromptProvider.csproj + TEST_PROJ: tests/PromptProvider.Tests/PromptProvider.Tests.csproj + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Verify projects exist + shell: bash + run: | + set -euo pipefail + [[ -f "$LIB_PROJ" ]] || { echo "Library not found: $LIB_PROJ"; exit 1; } + [[ -f "$TEST_PROJ" ]] || { echo "Test project not found: $TEST_PROJ"; exit 1; } + echo "Library: $LIB_PROJ" + echo "Tests: $TEST_PROJ" + + - name: Restore + shell: bash + run: | + dotnet restore "$TEST_PROJ" + + - name: Build library + shell: bash + run: | + dotnet build "$LIB_PROJ" -c Release -p:ContinuousIntegrationBuild=true --no-restore + + - name: Test + shell: bash + run: | + dotnet test "$TEST_PROJ" -c Release --logger "trx;LogFileName=test-results.trx" + + - name: Resolve version + id: ver + shell: bash + run: | + set -euo pipefail + if [ -n "${{ github.event.inputs.version }}" ]; then + PKGVER="${{ github.event.inputs.version }}" + else + PKGVER=$(grep -oPm1 '(?<=)[^<]+' "$LIB_PROJ" || true) + if [ -z "$PKGVER" ]; then + echo " not found in $LIB_PROJ" >&2 + exit 1 + fi + fi + echo "PKGVER=$PKGVER" >> $GITHUB_ENV + echo "Using version: $PKGVER" + + - name: Pack + shell: bash + run: | + dotnet pack "$LIB_PROJ" -c Release \ + -p:Version="${PKGVER}" \ + -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg \ + -o ./artifacts \ + --no-build + + - name: Push to nuget.org + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + shell: bash + run: | + set -euo pipefail + if [ -z "${NUGET_API_KEY:-}" ]; then + echo "NUGET_API_KEY secret is not set." >&2 + exit 1 + fi + dotnet nuget push "./artifacts/*.nupkg" --skip-duplicate --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json + if ls ./artifacts/*.snupkg 1> /dev/null 2>&1; then + dotnet nuget push "./artifacts/*.snupkg" --skip-duplicate --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json + else + echo "No symbol packages (.snupkg) found to push." + fi diff --git a/PromptProvider/DependencyInjection.cs b/DependencyInjection.cs similarity index 71% rename from PromptProvider/DependencyInjection.cs rename to DependencyInjection.cs index 0254148..6acfe6d 100644 --- a/PromptProvider/DependencyInjection.cs +++ b/DependencyInjection.cs @@ -13,11 +13,14 @@ public static IServiceCollection AddPromptProvider( Action? configurePrompts = null) { if (configureLangfuse is not null) services.Configure(configureLangfuse); - if (configurePrompts is not null) services.Configure(configurePrompts); + if (configurePrompts is not null) services.Configure(configurePrompts); services.AddHttpClient(); services.AddScoped(); + // Register default prompts provider (users may replace with custom implementation) + services.AddSingleton(); + return services; } } diff --git a/Interfaces/IDefaultPromptsProvider.cs b/Interfaces/IDefaultPromptsProvider.cs new file mode 100644 index 0000000..e9509e7 --- /dev/null +++ b/Interfaces/IDefaultPromptsProvider.cs @@ -0,0 +1,6 @@ +namespace PromptProvider.Interfaces; + +public interface IDefaultPromptsProvider +{ + IReadOnlyDictionary GetDefaults(); +} diff --git a/PromptProvider/Interfaces/ILangfuseService.cs b/Interfaces/ILangfuseService.cs similarity index 100% rename from PromptProvider/Interfaces/ILangfuseService.cs rename to Interfaces/ILangfuseService.cs diff --git a/PromptProvider/Interfaces/IPromptService.cs b/Interfaces/IPromptService.cs similarity index 100% rename from PromptProvider/Interfaces/IPromptService.cs rename to Interfaces/IPromptService.cs diff --git a/LICENSE b/LICENSE index 748de55..c198b1f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1 @@ -MIT License - -Copyright (c) 2025 ZioNet - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License (c) 2025 \ No newline at end of file diff --git a/PromptProvider/Models/CreateLangfusePromptRequest.cs b/Models/CreateLangfusePromptRequest.cs similarity index 100% rename from PromptProvider/Models/CreateLangfusePromptRequest.cs rename to Models/CreateLangfusePromptRequest.cs diff --git a/PromptProvider/Models/CreateLangfusePromptResponse.cs b/Models/CreateLangfusePromptResponse.cs similarity index 100% rename from PromptProvider/Models/CreateLangfusePromptResponse.cs rename to Models/CreateLangfusePromptResponse.cs diff --git a/PromptProvider/Models/CreatePromptRequest.cs b/Models/CreatePromptRequest.cs similarity index 100% rename from PromptProvider/Models/CreatePromptRequest.cs rename to Models/CreatePromptRequest.cs diff --git a/PromptProvider/Models/GetPromptsBatchRequest.cs b/Models/GetPromptsBatchRequest.cs similarity index 100% rename from PromptProvider/Models/GetPromptsBatchRequest.cs rename to Models/GetPromptsBatchRequest.cs diff --git a/PromptProvider/Models/GetPromptsBatchResponse.cs b/Models/GetPromptsBatchResponse.cs similarity index 100% rename from PromptProvider/Models/GetPromptsBatchResponse.cs rename to Models/GetPromptsBatchResponse.cs diff --git a/PromptProvider/Models/LangfusePromptListItem.cs b/Models/LangfusePromptListItem.cs similarity index 100% rename from PromptProvider/Models/LangfusePromptListItem.cs rename to Models/LangfusePromptListItem.cs diff --git a/PromptProvider/Models/LangfusePromptModel.cs b/Models/LangfusePromptModel.cs similarity index 100% rename from PromptProvider/Models/LangfusePromptModel.cs rename to Models/LangfusePromptModel.cs diff --git a/PromptProvider/Models/LangfusePromptsListResponse.cs b/Models/LangfusePromptsListResponse.cs similarity index 100% rename from PromptProvider/Models/LangfusePromptsListResponse.cs rename to Models/LangfusePromptsListResponse.cs diff --git a/PromptProvider/Models/PromptConfiguration.cs b/Models/PromptConfiguration.cs similarity index 100% rename from PromptProvider/Models/PromptConfiguration.cs rename to Models/PromptConfiguration.cs diff --git a/PromptProvider/Models/PromptModel.cs b/Models/PromptModel.cs similarity index 100% rename from PromptProvider/Models/PromptModel.cs rename to Models/PromptModel.cs diff --git a/PromptProvider/Models/PromptResponse.cs b/Models/PromptResponse.cs similarity index 100% rename from PromptProvider/Models/PromptResponse.cs rename to Models/PromptResponse.cs diff --git a/PromptProvider/Models/UpdatePromptLabelsRequest.cs b/Models/UpdatePromptLabelsRequest.cs similarity index 100% rename from PromptProvider/Models/UpdatePromptLabelsRequest.cs rename to Models/UpdatePromptLabelsRequest.cs diff --git a/PromptProvider/Options/LangfuseOptions.cs b/Options/LangfuseOptions.cs similarity index 100% rename from PromptProvider/Options/LangfuseOptions.cs rename to Options/LangfuseOptions.cs diff --git a/PromptProvider/Options/PromptConfiguration.cs b/Options/PromptConfiguration.cs similarity index 100% rename from PromptProvider/Options/PromptConfiguration.cs rename to Options/PromptConfiguration.cs diff --git a/PromptProvider/Options/PromptsOptions.cs b/Options/PromptsOptions.cs similarity index 100% rename from PromptProvider/Options/PromptsOptions.cs rename to Options/PromptsOptions.cs diff --git a/PromptProvider.csproj b/PromptProvider.csproj new file mode 100644 index 0000000..a047dce --- /dev/null +++ b/PromptProvider.csproj @@ -0,0 +1,29 @@ + + + net9.0 + enable + enable + true + PromptProvider + 1.0.0 + Author + PromptProvider library with Langfuse integration and default prompts support. + prompts;langfuse;configuration + https://example.com/your-repo + MIT + + + + + + + + + + + + + + + + diff --git a/PromptProvider/PromptProvider.sln b/PromptProvider.sln similarity index 100% rename from PromptProvider/PromptProvider.sln rename to PromptProvider.sln diff --git a/PromptProvider/LICENSE b/PromptProvider/LICENSE deleted file mode 100644 index c198b1f..0000000 --- a/PromptProvider/LICENSE +++ /dev/null @@ -1 +0,0 @@ -MIT License (c) 2025 \ No newline at end of file diff --git a/PromptProvider/Options/PromptKeyOptions.cs b/PromptProvider/Options/PromptKeyOptions.cs deleted file mode 100644 index 306b87d..0000000 --- a/PromptProvider/Options/PromptKeyOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace PromptProvider.Options; - - -public sealed class PromptKeyOptions -{ - public PromptConfiguration? ChatTitlePrompt { get; set; } - public PromptConfiguration? SystemDefault { get; set; } - public PromptConfiguration? FriendlyTone { get; set; } - public PromptConfiguration? DetailedExplanation { get; set; } - public PromptConfiguration? ExplainMistakeSystem { get; set; } - public PromptConfiguration? GlobalChatSystemDefault { get; set; } - public PromptConfiguration? GlobalChatPageContext { get; set; } - public PromptConfiguration? MistakeUserTemplate { get; set; } - public PromptConfiguration? MistakeRuleTemplate { get; set; } -} \ No newline at end of file diff --git a/PromptProvider/PromptProvider.csproj b/PromptProvider/PromptProvider.csproj deleted file mode 100644 index 60463b8..0000000 --- a/PromptProvider/PromptProvider.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net9.0 - enable - enable - - - - - - - - - - diff --git a/PromptProvider/README.md b/PromptProvider/README.md deleted file mode 100644 index 067e2bf..0000000 --- a/PromptProvider/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# PromptProvider - -Full Langfuse integration with local prompt fallback. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbf1afd --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# PromptProvider + +PromptProvider — a library for managing prompts with Langfuse integration and support for default prompts. + +## Installation + +```powershell +dotnet add package PromptProvider +``` + +## Usage + +```csharp +services.AddPromptProvider( + configureLangfuse: options => Configuration.GetSection("Langfuse").Bind(options), + configurePrompts: options => Configuration.GetSection("Prompts").Bind(options) +); +``` + +If you want to provide default prompts from configuration, register a provider or configure `Prompts` section and use the built-in `ConfigurationDefaultPromptsProvider`. + +## Configuration example + +`appsettings.json` example for `Prompts` section: + +```json +{ + "Prompts": { + "Defaults": { + "WelcomePrompt": "Welcome to our system!", + "ErrorPrompt": "An error occurred. Please try again." + } + } +} \ No newline at end of file diff --git a/Services/ConfigurationDefaultPromptsProvider.cs b/Services/ConfigurationDefaultPromptsProvider.cs new file mode 100644 index 0000000..c381baf --- /dev/null +++ b/Services/ConfigurationDefaultPromptsProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Options; +using PromptProvider.Interfaces; +using PromptProvider.Options; + +namespace PromptProvider.Services; + +public class ConfigurationDefaultPromptsProvider : IDefaultPromptsProvider +{ + private readonly PromptsOptions _options; + + public ConfigurationDefaultPromptsProvider(IOptions options) + { + _options = options.Value; + } + + public IReadOnlyDictionary GetDefaults() + { + return _options.Defaults; + } +} diff --git a/PromptProvider/Services/LangfuseService.cs b/Services/LangfuseService.cs similarity index 100% rename from PromptProvider/Services/LangfuseService.cs rename to Services/LangfuseService.cs diff --git a/PromptProvider/Services/PromptService.cs b/Services/PromptService.cs similarity index 95% rename from PromptProvider/Services/PromptService.cs rename to Services/PromptService.cs index 902c8ef..6d26fa1 100644 --- a/PromptProvider/Services/PromptService.cs +++ b/Services/PromptService.cs @@ -1,8 +1,7 @@ using PromptProvider.Models; -using PromptProvider.Options; using PromptProvider.Interfaces; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using PromptProvider.Options; namespace PromptProvider.Services; @@ -10,18 +9,18 @@ public class PromptService : IPromptService { private readonly ILogger _logger; private readonly ILangfuseService _langfuseService; - private readonly IOptions _promptsOptions; - private readonly IOptions _langfuseOptions; + private readonly IDefaultPromptsProvider _defaultPromptsProvider; + private readonly Microsoft.Extensions.Options.IOptions _langfuseOptions; public PromptService( ILogger logger, ILangfuseService langfuseService, - IOptions promptsOptions, - IOptions langfuseOptions) + IDefaultPromptsProvider defaultPromptsProvider, + Microsoft.Extensions.Options.IOptions langfuseOptions) { _logger = logger; _langfuseService = langfuseService; - _promptsOptions = promptsOptions; + _defaultPromptsProvider = defaultPromptsProvider; _langfuseOptions = langfuseOptions; } @@ -236,7 +235,7 @@ public async Task UpdatePromptLabelsAsync( private PromptResponse? GetPromptFromDefaults(string promptKey) { - var defaults = _promptsOptions.Value.Defaults; + var defaults = _defaultPromptsProvider.GetDefaults(); if (defaults == null || !defaults.TryGetValue(promptKey, out var content)) { _logger.LogWarning("Prompt '{PromptKey}' not found in local defaults either", promptKey); diff --git a/tests/PromptProvider.Tests/PromptProvider.Tests.csproj b/tests/PromptProvider.Tests/PromptProvider.Tests.csproj new file mode 100644 index 0000000..29dbef1 --- /dev/null +++ b/tests/PromptProvider.Tests/PromptProvider.Tests.csproj @@ -0,0 +1,18 @@ + + + net9.0 + false + 13.0 + + + + + + + + + + + + + diff --git a/tests/PromptProvider.Tests/PromptServiceTests.cs b/tests/PromptProvider.Tests/PromptServiceTests.cs new file mode 100644 index 0000000..87a3f68 --- /dev/null +++ b/tests/PromptProvider.Tests/PromptServiceTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using PromptProvider.Interfaces; +using PromptProvider.Models; +using PromptProvider.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using PromptProvider.Options; +using Xunit; + +namespace PromptProvider.Tests; + +public class PromptServiceTests +{ + [Fact] + public async Task GetPromptAsync_FallbacksToDefaults_WhenLangfuseUnavailable() + { + var langfuseMock = new Mock(); + langfuseMock.Setup(l => l.GetPromptAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException("not configured")); + + var defaultsProviderMock = new Mock(); + defaultsProviderMock.Setup(p => p.GetDefaults()).Returns(new System.Collections.Generic.Dictionary + { + { "WelcomePrompt", "Welcome!" } + }); + + var langfuseOptions = Options.Create(new LangfuseOptions()); + + var service = new PromptService( + new NullLogger(), + langfuseMock.Object, + defaultsProviderMock.Object, + langfuseOptions + ); + + var result = await service.GetPromptAsync("WelcomePrompt"); + + Assert.NotNull(result); + Assert.Equal("WelcomePrompt", result!.PromptKey); + Assert.Equal("Welcome!", result.Content); + Assert.Equal("Local", result.Source); + } + + [Fact] + public async Task GetPromptAsync_ReturnsLangfuseResult_WhenAvailable() + { + var langfuseMock = new Mock(); + langfuseMock.Setup(l => l.GetPromptAsync("Key1", null, null, It.IsAny())) + .ReturnsAsync(new LangfusePromptModel { Name = "Key1", Prompt = "From Langfuse", Version = 1, Type = "text", Labels = Array.Empty(), Tags = Array.Empty() }); + + var defaultsProviderMock = new Mock(); + defaultsProviderMock.Setup(p => p.GetDefaults()).Returns(new System.Collections.Generic.Dictionary()); + + var langfuseOptions = Options.Create(new LangfuseOptions { BaseUrl = "https://x", PublicKey = "a", SecretKey = "b" }); + + var service = new PromptService( + new NullLogger(), + langfuseMock.Object, + defaultsProviderMock.Object, + langfuseOptions + ); + + var result = await service.GetPromptAsync("Key1"); + + Assert.NotNull(result); + Assert.Equal("Key1", result!.PromptKey); + Assert.Equal("From Langfuse", result.Content); + Assert.Equal("Langfuse", result.Source); + } +} From 01044681d2b647a7221dd2a56b5e67d5751de778 Mon Sep 17 00:00:00 2001 From: KrastyTC Date: Tue, 18 Nov 2025 17:03:58 +0200 Subject: [PATCH 03/12] Update publish-nuget.yml --- .github/workflows/publish-nuget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 9526b26..97375c1 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -16,7 +16,7 @@ jobs: env: LIB_PROJ: PromptProvider/PromptProvider.csproj - TEST_PROJ: tests/PromptProvider.Tests/PromptProvider.Tests.csproj + TEST_PROJ: Tests/PromptProvider.Tests/PromptProvider.Tests.csproj steps: - uses: actions/checkout@v4 From ccef8f0bf2d7db51c3103788f6ee21109ddf16ca Mon Sep 17 00:00:00 2001 From: KrastyTC Date: Wed, 19 Nov 2025 15:53:27 +0200 Subject: [PATCH 04/12] Remove PromptProvider test project and tests --- .../PromptProvider.Tests.csproj | 18 ----- .../PromptServiceTests.cs | 73 ------------------- 2 files changed, 91 deletions(-) delete mode 100644 tests/PromptProvider.Tests/PromptProvider.Tests.csproj delete mode 100644 tests/PromptProvider.Tests/PromptServiceTests.cs diff --git a/tests/PromptProvider.Tests/PromptProvider.Tests.csproj b/tests/PromptProvider.Tests/PromptProvider.Tests.csproj deleted file mode 100644 index 29dbef1..0000000 --- a/tests/PromptProvider.Tests/PromptProvider.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - net9.0 - false - 13.0 - - - - - - - - - - - - - diff --git a/tests/PromptProvider.Tests/PromptServiceTests.cs b/tests/PromptProvider.Tests/PromptServiceTests.cs deleted file mode 100644 index 87a3f68..0000000 --- a/tests/PromptProvider.Tests/PromptServiceTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Moq; -using PromptProvider.Interfaces; -using PromptProvider.Models; -using PromptProvider.Services; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using PromptProvider.Options; -using Xunit; - -namespace PromptProvider.Tests; - -public class PromptServiceTests -{ - [Fact] - public async Task GetPromptAsync_FallbacksToDefaults_WhenLangfuseUnavailable() - { - var langfuseMock = new Mock(); - langfuseMock.Setup(l => l.GetPromptAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new InvalidOperationException("not configured")); - - var defaultsProviderMock = new Mock(); - defaultsProviderMock.Setup(p => p.GetDefaults()).Returns(new System.Collections.Generic.Dictionary - { - { "WelcomePrompt", "Welcome!" } - }); - - var langfuseOptions = Options.Create(new LangfuseOptions()); - - var service = new PromptService( - new NullLogger(), - langfuseMock.Object, - defaultsProviderMock.Object, - langfuseOptions - ); - - var result = await service.GetPromptAsync("WelcomePrompt"); - - Assert.NotNull(result); - Assert.Equal("WelcomePrompt", result!.PromptKey); - Assert.Equal("Welcome!", result.Content); - Assert.Equal("Local", result.Source); - } - - [Fact] - public async Task GetPromptAsync_ReturnsLangfuseResult_WhenAvailable() - { - var langfuseMock = new Mock(); - langfuseMock.Setup(l => l.GetPromptAsync("Key1", null, null, It.IsAny())) - .ReturnsAsync(new LangfusePromptModel { Name = "Key1", Prompt = "From Langfuse", Version = 1, Type = "text", Labels = Array.Empty(), Tags = Array.Empty() }); - - var defaultsProviderMock = new Mock(); - defaultsProviderMock.Setup(p => p.GetDefaults()).Returns(new System.Collections.Generic.Dictionary()); - - var langfuseOptions = Options.Create(new LangfuseOptions { BaseUrl = "https://x", PublicKey = "a", SecretKey = "b" }); - - var service = new PromptService( - new NullLogger(), - langfuseMock.Object, - defaultsProviderMock.Object, - langfuseOptions - ); - - var result = await service.GetPromptAsync("Key1"); - - Assert.NotNull(result); - Assert.Equal("Key1", result!.PromptKey); - Assert.Equal("From Langfuse", result.Content); - Assert.Equal("Langfuse", result.Source); - } -} From a47eb0f6338d1016a067fa1e2fe0f26853c78143 Mon Sep 17 00:00:00 2001 From: KrastyTC Date: Wed, 19 Nov 2025 15:59:51 +0200 Subject: [PATCH 05/12] Delete ci.yml --- .github/workflows/ci.yml | 51 ---------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 83dc2f6..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CI - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - workflow_dispatch: {} - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - name: Restore - run: dotnet restore - - name: Build - run: dotnet build --configuration Release --no-restore - - name: Test - run: dotnet test --no-build --configuration Release - - name: Pack - run: dotnet pack PromptProvider/PromptProvider.csproj -c Release -o ./artifacts - - publish: - needs: build - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - name: Restore - run: dotnet restore - - name: Pack - run: dotnet pack PromptProvider/PromptProvider.csproj -c Release -o ./artifacts - - name: Publish to NuGet - uses: NuGet/setup-nuget@v2 - - name: Push package - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: | - for pkg in ./artifacts/*.nupkg; do - dotnet nuget push "$pkg" -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json - done From 28562c0fc0433f66faf0e0c6aa3f8a3803328d99 Mon Sep 17 00:00:00 2001 From: Alexander Kulyabin <108358930+KrastyTC@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:03:08 +0200 Subject: [PATCH 06/12] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dbf1afd..bc3fb47 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PromptProvider -PromptProvider — a library for managing prompts with Langfuse integration and support for default prompts. +PromptProvider - a library for managing prompts with Langfuse integration and support for default prompts. ## Installation From 6020d166b13633d16f2861f70a8e5ee9e659ef72 Mon Sep 17 00:00:00 2001 From: Alexander Kulyabin <108358930+KrastyTC@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:03:50 +0200 Subject: [PATCH 07/12] Update DependencyInjection.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DependencyInjection.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DependencyInjection.cs b/DependencyInjection.cs index 6acfe6d..20c2a91 100644 --- a/DependencyInjection.cs +++ b/DependencyInjection.cs @@ -15,7 +15,14 @@ public static IServiceCollection AddPromptProvider( if (configureLangfuse is not null) services.Configure(configureLangfuse); if (configurePrompts is not null) services.Configure(configurePrompts); - services.AddHttpClient(); + services.AddHttpClient((serviceProvider, client) => + { + var options = serviceProvider.GetRequiredService>().Value; + if (options.IsConfigured() && !string.IsNullOrWhiteSpace(options.BaseUrl)) + { + client.BaseAddress = new Uri(options.BaseUrl); + } + }); services.AddScoped(); // Register default prompts provider (users may replace with custom implementation) From 09b45d96e6679f1c7525edd9fd453439d5a95779 Mon Sep 17 00:00:00 2001 From: Alexander Kulyabin <108358930+KrastyTC@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:04:23 +0200 Subject: [PATCH 08/12] Update Services/LangfuseService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Services/LangfuseService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Services/LangfuseService.cs b/Services/LangfuseService.cs index a392304..840be26 100644 --- a/Services/LangfuseService.cs +++ b/Services/LangfuseService.cs @@ -47,6 +47,7 @@ private void ConfigureHttpClient() var authValue = Convert.ToBase64String( Encoding.UTF8.GetBytes($"{_options.PublicKey}:{_options.SecretKey}")); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue); + _httpClient.BaseAddress = new Uri(_options.BaseUrl); } private void ThrowIfNotConfigured() From 24b06f8c82a800c585b97142dff629024eb24f53 Mon Sep 17 00:00:00 2001 From: Alexander Kulyabin <108358930+KrastyTC@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:05:08 +0200 Subject: [PATCH 09/12] Update LICENSE Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- LICENSE | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c198b1f..f4a800f 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,21 @@ -MIT License (c) 2025 \ No newline at end of file +MIT License + +Copyright (c) 2025 [copyright holder] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From 6c5a4a9e56237d531caffd4cc184fba24004c360 Mon Sep 17 00:00:00 2001 From: Alexander Kulyabin <108358930+KrastyTC@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:05:38 +0200 Subject: [PATCH 10/12] Update Options/PromptConfiguration.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Options/PromptConfiguration.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Options/PromptConfiguration.cs b/Options/PromptConfiguration.cs index 4e88152..e69de29 100644 --- a/Options/PromptConfiguration.cs +++ b/Options/PromptConfiguration.cs @@ -1,20 +0,0 @@ -namespace PromptProvider.Options; - -public sealed class PromptConfiguration -{ - /// - /// The prompt key/name - /// - public required string Key { get; set; } - - /// - /// Optional specific version number to use - /// - public int? Version { get; set; } - - /// - /// Optional label to use (e.g., "production", "staging", "latest") - /// If neither version nor label is specified, defaults to "production" - /// - public string? Label { get; set; } -} From 95e6d9059cc62f1dc4012dafbf5f22715e99702b Mon Sep 17 00:00:00 2001 From: Alexander Kulyabin <108358930+KrastyTC@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:06:27 +0200 Subject: [PATCH 11/12] Update Services/LangfuseService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Services/LangfuseService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/LangfuseService.cs b/Services/LangfuseService.cs index 840be26..66000b9 100644 --- a/Services/LangfuseService.cs +++ b/Services/LangfuseService.cs @@ -204,7 +204,7 @@ public async Task CreatePromptAsync( _logger.LogInformation("Creating new prompt version for '{PromptName}' in Langfuse", request.Name); var jsonContent = JsonSerializer.Serialize(request, JsonOptions); - var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + using var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("/api/public/v2/prompts", httpContent, cancellationToken); response.EnsureSuccessStatusCode(); From ba082e1edbfaa8cfa7ec3a7483ba1516178ab89d Mon Sep 17 00:00:00 2001 From: KrastyTC Date: Wed, 19 Nov 2025 16:07:01 +0200 Subject: [PATCH 12/12] Update publish-nuget.yml --- .github/workflows/publish-nuget.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 97375c1..4b30da9 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -30,10 +30,7 @@ jobs: run: | set -euo pipefail [[ -f "$LIB_PROJ" ]] || { echo "Library not found: $LIB_PROJ"; exit 1; } - [[ -f "$TEST_PROJ" ]] || { echo "Test project not found: $TEST_PROJ"; exit 1; } echo "Library: $LIB_PROJ" - echo "Tests: $TEST_PROJ" - - name: Restore shell: bash run: | @@ -43,12 +40,6 @@ jobs: shell: bash run: | dotnet build "$LIB_PROJ" -c Release -p:ContinuousIntegrationBuild=true --no-restore - - - name: Test - shell: bash - run: | - dotnet test "$TEST_PROJ" -c Release --logger "trx;LogFileName=test-results.trx" - - name: Resolve version id: ver shell: bash