| | | 1 | | using OpenAiIntegration; |
| | | 2 | | |
| | | 3 | | namespace Orchestrator.Infrastructure.Langfuse; |
| | | 4 | | |
| | | 5 | | internal enum LangfusePromptKind |
| | | 6 | | { |
| | | 7 | | Match, |
| | | 8 | | Bonus |
| | | 9 | | } |
| | | 10 | | |
| | | 11 | | internal sealed class LangfuseTextPromptTemplateProvider : IInstructionsTemplateProvider, IPromptTemplateTelemetryMetada |
| | | 12 | | { |
| | | 13 | | private readonly ILangfusePublicApiClient _client; |
| | | 14 | | private readonly string _promptName; |
| | | 15 | | private readonly string? _label; |
| | | 16 | | private readonly int? _version; |
| | | 17 | | private readonly LangfusePrompt? _preloadedPrompt; |
| | | 18 | | private readonly LangfusePromptKind _promptKind; |
| | | 19 | | private readonly IInstructionsTemplateProvider? _fallbackTemplateProvider; |
| | | 20 | | private readonly string? _fallbackModel; |
| | | 21 | | private readonly Action<string>? _fallbackWarning; |
| | | 22 | | private readonly Lazy<ResolvedPrompt> _prompt; |
| | | 23 | | |
| | 1 | 24 | | public LangfuseTextPromptTemplateProvider( |
| | 1 | 25 | | ILangfusePublicApiClient client, |
| | 1 | 26 | | string promptName, |
| | 1 | 27 | | string? label, |
| | 1 | 28 | | int? version, |
| | 1 | 29 | | LangfusePrompt? preloadedPrompt = null, |
| | 1 | 30 | | LangfusePromptKind promptKind = LangfusePromptKind.Match, |
| | 1 | 31 | | IInstructionsTemplateProvider? fallbackTemplateProvider = null, |
| | 1 | 32 | | string? fallbackModel = null, |
| | 1 | 33 | | Action<string>? fallbackWarning = null) |
| | | 34 | | { |
| | 1 | 35 | | _client = client ?? throw new ArgumentNullException(nameof(client)); |
| | 1 | 36 | | _promptName = string.IsNullOrWhiteSpace(promptName) |
| | 1 | 37 | | ? throw new ArgumentException("Langfuse prompt name must be provided.", nameof(promptName)) |
| | 1 | 38 | | : promptName.Trim(); |
| | 1 | 39 | | _label = string.IsNullOrWhiteSpace(label) ? null : label.Trim(); |
| | 1 | 40 | | _version = version; |
| | 1 | 41 | | _preloadedPrompt = preloadedPrompt; |
| | 1 | 42 | | _promptKind = promptKind; |
| | 1 | 43 | | _fallbackTemplateProvider = fallbackTemplateProvider; |
| | 1 | 44 | | _fallbackModel = string.IsNullOrWhiteSpace(fallbackModel) ? null : fallbackModel.Trim(); |
| | 1 | 45 | | _fallbackWarning = fallbackWarning; |
| | 1 | 46 | | _prompt = new Lazy<ResolvedPrompt>(LoadPrompt); |
| | 1 | 47 | | } |
| | | 48 | | |
| | 0 | 49 | | public LangfusePrompt? Prompt => _prompt.Value.Prompt; |
| | | 50 | | |
| | | 51 | | public PromptTemplateTelemetryMetadata? GetPromptTemplateTelemetryMetadata() |
| | | 52 | | { |
| | 1 | 53 | | return _prompt.IsValueCreated ? _prompt.Value.TelemetryMetadata : null; |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | public (string template, string path) LoadMatchTemplate(string model, bool includeJustification) |
| | | 57 | | { |
| | 1 | 58 | | if (_promptKind != LangfusePromptKind.Match) |
| | | 59 | | { |
| | 0 | 60 | | throw new NotSupportedException("This Langfuse prompt provider is configured for bonus prompts."); |
| | | 61 | | } |
| | | 62 | | |
| | 1 | 63 | | if (includeJustification) |
| | | 64 | | { |
| | 1 | 65 | | throw new NotSupportedException( |
| | 1 | 66 | | "The Langfuse prompt source only supports WM 2026 match prompts without justification in this version.") |
| | | 67 | | } |
| | | 68 | | |
| | 1 | 69 | | var prompt = _prompt.Value; |
| | 1 | 70 | | return (prompt.Template, prompt.Path); |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | public (string template, string path) LoadBonusTemplate(string model) |
| | | 74 | | { |
| | 1 | 75 | | if (_promptKind != LangfusePromptKind.Bonus) |
| | | 76 | | { |
| | 0 | 77 | | throw new NotSupportedException("This Langfuse prompt provider is configured for match prompts."); |
| | | 78 | | } |
| | | 79 | | |
| | 1 | 80 | | var prompt = _prompt.Value; |
| | 1 | 81 | | return (prompt.Template, prompt.Path); |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | private ResolvedPrompt LoadPrompt() |
| | | 85 | | { |
| | | 86 | | try |
| | | 87 | | { |
| | 1 | 88 | | var prompt = _preloadedPrompt |
| | 1 | 89 | | ?? _client.GetPromptAsync(_promptName, _label, _version) |
| | 1 | 90 | | .GetAwaiter() |
| | 1 | 91 | | .GetResult(); |
| | | 92 | | |
| | 1 | 93 | | if (prompt is not null) |
| | | 94 | | { |
| | 1 | 95 | | var path = BuildPromptPath(prompt); |
| | 1 | 96 | | return new ResolvedPrompt( |
| | 1 | 97 | | prompt.GetTextPrompt(), |
| | 1 | 98 | | path, |
| | 1 | 99 | | prompt, |
| | 1 | 100 | | new PromptTemplateTelemetryMetadata(prompt.Name, prompt.Version, IsFallback: false, path)); |
| | | 101 | | } |
| | | 102 | | |
| | 1 | 103 | | return LoadFallbackPrompt($"Langfuse prompt '{_promptName}' was not found."); |
| | | 104 | | } |
| | 1 | 105 | | catch (Exception ex) when (_fallbackTemplateProvider is not null) |
| | | 106 | | { |
| | 1 | 107 | | return LoadFallbackPrompt($"Failed to fetch Langfuse prompt '{_promptName}': {ex.Message}"); |
| | | 108 | | } |
| | 1 | 109 | | } |
| | | 110 | | |
| | | 111 | | private string BuildPromptPath(LangfusePrompt prompt) |
| | | 112 | | { |
| | 1 | 113 | | var labelSuffix = string.IsNullOrWhiteSpace(_label) ? string.Empty : $"?label={Uri.EscapeDataString(_label)}"; |
| | 1 | 114 | | return $"langfuse://prompts/{Uri.EscapeDataString(prompt.Name)}/versions/{prompt.Version}{labelSuffix}"; |
| | | 115 | | } |
| | | 116 | | |
| | | 117 | | private ResolvedPrompt LoadFallbackPrompt(string reason) |
| | | 118 | | { |
| | 1 | 119 | | if (_fallbackTemplateProvider is null || string.IsNullOrWhiteSpace(_fallbackModel)) |
| | | 120 | | { |
| | 0 | 121 | | throw new FileNotFoundException( |
| | 0 | 122 | | $"{reason} No local fallback prompt was configured for '{_promptName}'."); |
| | | 123 | | } |
| | | 124 | | |
| | 1 | 125 | | var fallback = _promptKind == LangfusePromptKind.Match |
| | 1 | 126 | | ? _fallbackTemplateProvider.LoadMatchTemplate(_fallbackModel, includeJustification: false) |
| | 1 | 127 | | : _fallbackTemplateProvider.LoadBonusTemplate(_fallbackModel); |
| | | 128 | | |
| | 1 | 129 | | _fallbackWarning?.Invoke($"{reason} Using local fallback prompt '{fallback.path}'."); |
| | 1 | 130 | | return new ResolvedPrompt( |
| | 1 | 131 | | fallback.template, |
| | 1 | 132 | | fallback.path, |
| | 1 | 133 | | Prompt: null, |
| | 1 | 134 | | new PromptTemplateTelemetryMetadata(_promptName, null, IsFallback: true, fallback.path)); |
| | | 135 | | } |
| | | 136 | | |
| | 1 | 137 | | private sealed record ResolvedPrompt( |
| | 1 | 138 | | string Template, |
| | 1 | 139 | | string Path, |
| | 1 | 140 | | LangfusePrompt? Prompt, |
| | 1 | 141 | | PromptTemplateTelemetryMetadata TelemetryMetadata); |
| | | 142 | | } |