Skip to content

Commit 7c0b627

Browse files
Exposes Dev Proxy LM prompts. Closes #1247 (#1249)
1 parent fea3903 commit 7c0b627

17 files changed

+408
-43
lines changed

DevProxy.Abstractions/DevProxy.Abstractions.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414

1515
<ItemGroup>
1616
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
17+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
1718
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
1819
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
1920
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
2021
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
2122
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
2223
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.1" />
24+
<PackageReference Include="Prompty.Core" Version="0.2.2-beta" />
2325
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
2426
<PackageReference Include="Unobtanium.Web.Proxy" Version="0.1.5" />
2527
</ItemGroup>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.Abstractions.Utils;
6+
using Microsoft.Extensions.AI;
7+
using Microsoft.Extensions.Logging;
8+
using PromptyCore = Prompty.Core;
9+
using System.Collections.Concurrent;
10+
11+
namespace DevProxy.Abstractions.LanguageModel;
12+
13+
public abstract class BaseLanguageModelClient(ILogger logger) : ILanguageModelClient
14+
{
15+
private readonly ILogger _logger = logger;
16+
private readonly ConcurrentDictionary<string, (IEnumerable<ILanguageModelChatCompletionMessage>?, CompletionOptions?)> _promptCache = new();
17+
18+
public virtual async Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(string promptFileName, Dictionary<string, object> parameters)
19+
{
20+
ArgumentNullException.ThrowIfNull(promptFileName, nameof(promptFileName));
21+
22+
if (!promptFileName.EndsWith(".prompty", StringComparison.OrdinalIgnoreCase))
23+
{
24+
_logger.LogDebug("Prompt file name '{PromptFileName}' does not end with '.prompty'. Appending the extension.", promptFileName);
25+
promptFileName += ".prompty";
26+
}
27+
var (messages, options) = _promptCache.GetOrAdd(promptFileName, _ =>
28+
LoadPrompt(promptFileName, parameters));
29+
30+
if (messages is null || !messages.Any())
31+
{
32+
return null;
33+
}
34+
35+
return await GenerateChatCompletionAsync(messages, options);
36+
}
37+
38+
public virtual Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(IEnumerable<ILanguageModelChatCompletionMessage> messages, CompletionOptions? options = null) => throw new NotImplementedException();
39+
40+
public virtual Task<ILanguageModelCompletionResponse?> GenerateCompletionAsync(string prompt, CompletionOptions? options = null) => throw new NotImplementedException();
41+
42+
public virtual Task<bool> IsEnabledAsync() => throw new NotImplementedException();
43+
44+
protected virtual IEnumerable<ILanguageModelChatCompletionMessage> ConvertMessages(ChatMessage[] messages) => throw new NotImplementedException();
45+
46+
private (IEnumerable<ILanguageModelChatCompletionMessage>?, CompletionOptions?) LoadPrompt(string promptFileName, Dictionary<string, object> parameters)
47+
{
48+
_logger.LogDebug("Prompt file {PromptFileName} not in the cache. Loading...", promptFileName);
49+
50+
var filePath = Path.Combine(ProxyUtils.AppFolder!, "prompts", promptFileName);
51+
if (!File.Exists(filePath))
52+
{
53+
throw new FileNotFoundException($"Prompt file '{filePath}' not found.");
54+
}
55+
56+
_logger.LogDebug("Loading prompt file: {FilePath}", filePath);
57+
var promptContents = File.ReadAllText(filePath);
58+
59+
var prompty = PromptyCore.Prompty.Load(promptContents, []);
60+
if (prompty.Prepare(parameters) is not ChatMessage[] promptyMessages ||
61+
promptyMessages.Length == 0)
62+
{
63+
_logger.LogError("No messages found in the prompt file: {FilePath}", filePath);
64+
return (null, null);
65+
}
66+
67+
var messages = ConvertMessages(promptyMessages);
68+
69+
var options = new CompletionOptions();
70+
if (prompty.Model?.Options is not null)
71+
{
72+
if (prompty.Model.Options.TryGetValue("temperature", out var temperature))
73+
{
74+
options.Temperature = temperature as double?;
75+
}
76+
if (prompty.Model.Options.TryGetValue("top_p", out var topP))
77+
{
78+
options.TopP = topP as double?;
79+
}
80+
}
81+
82+
return (messages, options);
83+
}
84+
}

DevProxy.Abstractions/LanguageModel/CompletionOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ public class CompletionOptions
1010
{
1111
[JsonPropertyName("temperature")]
1212
public double? Temperature { get; set; }
13+
[JsonPropertyName("top_p")]
14+
public double? TopP { get; set; }
1315
}

DevProxy.Abstractions/LanguageModel/ILanguageModelClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace DevProxy.Abstractions.LanguageModel;
66

77
public interface ILanguageModelClient
88
{
9+
Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(string promptFileName, Dictionary<string, object> parameters);
910
Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(IEnumerable<ILanguageModelChatCompletionMessage> messages, CompletionOptions? options = null);
1011
Task<ILanguageModelCompletionResponse?> GenerateCompletionAsync(string prompt, CompletionOptions? options = null);
1112
Task<bool> IsEnabledAsync();

DevProxy.Abstractions/LanguageModel/LanguageModelClientFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Prompty.Core;
78

89
namespace DevProxy.Abstractions.LanguageModel;
910

@@ -17,6 +18,8 @@ public static ILanguageModelClient Create(IServiceProvider serviceProvider, ICon
1718
var lmSection = configuration.GetSection("LanguageModel");
1819
var config = lmSection?.Get<LanguageModelConfiguration>() ?? new();
1920

21+
InvokerFactory.AutoDiscovery();
22+
2023
return config.Client switch
2124
{
2225
LanguageModelClient.Ollama => ActivatorUtilities.CreateInstance<OllamaLanguageModelClient>(serviceProvider, config),

DevProxy.Abstractions/LanguageModel/OllamaLanguageModelClient.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
using System.Diagnostics;
66
using System.Net.Http.Json;
7+
using Microsoft.Extensions.AI;
78
using Microsoft.Extensions.Logging;
89

910
namespace DevProxy.Abstractions.LanguageModel;
1011

1112
public sealed class OllamaLanguageModelClient(
1213
HttpClient httpClient,
1314
LanguageModelConfiguration configuration,
14-
ILogger<OllamaLanguageModelClient> logger) : ILanguageModelClient
15+
ILogger<OllamaLanguageModelClient> logger) : BaseLanguageModelClient(logger)
1516
{
1617
private readonly LanguageModelConfiguration _configuration = configuration;
1718
private readonly ILogger _logger = logger;
@@ -20,7 +21,7 @@ public sealed class OllamaLanguageModelClient(
2021
private readonly Dictionary<IEnumerable<ILanguageModelChatCompletionMessage>, OllamaLanguageModelChatCompletionResponse> _cacheChatCompletion = [];
2122
private bool? _lmAvailable;
2223

23-
public async Task<bool> IsEnabledAsync()
24+
public override async Task<bool> IsEnabledAsync()
2425
{
2526
if (_lmAvailable.HasValue)
2627
{
@@ -31,7 +32,7 @@ public async Task<bool> IsEnabledAsync()
3132
return _lmAvailable.Value;
3233
}
3334

34-
public async Task<ILanguageModelCompletionResponse?> GenerateCompletionAsync(string prompt, CompletionOptions? options = null)
35+
public override async Task<ILanguageModelCompletionResponse?> GenerateCompletionAsync(string prompt, CompletionOptions? options = null)
3536
{
3637
using var scope = _logger.BeginScope(nameof(OllamaLanguageModelClient));
3738

@@ -78,7 +79,7 @@ public async Task<bool> IsEnabledAsync()
7879
}
7980
}
8081

81-
public async Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(IEnumerable<ILanguageModelChatCompletionMessage> messages, CompletionOptions? options = null)
82+
public override async Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(IEnumerable<ILanguageModelChatCompletionMessage> messages, CompletionOptions? options = null)
8283
{
8384
using var scope = _logger.BeginScope(nameof(OllamaLanguageModelClient));
8485

@@ -125,6 +126,15 @@ public async Task<bool> IsEnabledAsync()
125126
}
126127
}
127128

129+
protected override IEnumerable<ILanguageModelChatCompletionMessage> ConvertMessages(ChatMessage[] messages)
130+
{
131+
return messages.Select(m => new OllamaLanguageModelChatCompletionMessage
132+
{
133+
Role = m.Role.Value,
134+
Content = m.Text
135+
});
136+
}
137+
128138
private async Task<bool> IsEnabledInternalAsync()
129139
{
130140
if (_configuration is null || !_configuration.Enabled)

DevProxy.Abstractions/LanguageModel/OpenAILanguageModelClient.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using DevProxy.Abstractions.Utils;
6+
using Microsoft.Extensions.AI;
67
using Microsoft.Extensions.Logging;
78
using System.Diagnostics;
89
using System.Net.Http.Json;
@@ -12,15 +13,15 @@ namespace DevProxy.Abstractions.LanguageModel;
1213
public sealed class OpenAILanguageModelClient(
1314
HttpClient httpClient,
1415
LanguageModelConfiguration configuration,
15-
ILogger<OpenAILanguageModelClient> logger) : ILanguageModelClient
16+
ILogger<OpenAILanguageModelClient> logger) : BaseLanguageModelClient(logger)
1617
{
1718
private readonly LanguageModelConfiguration? _configuration = configuration;
1819
private readonly HttpClient _httpClient = httpClient;
1920
private readonly ILogger _logger = logger;
2021
private readonly Dictionary<IEnumerable<ILanguageModelChatCompletionMessage>, OpenAIChatCompletionResponse> _cacheChatCompletion = [];
2122
private bool? _lmAvailable;
2223

23-
public async Task<bool> IsEnabledAsync()
24+
public override async Task<bool> IsEnabledAsync()
2425
{
2526
if (_lmAvailable.HasValue)
2627
{
@@ -31,7 +32,7 @@ public async Task<bool> IsEnabledAsync()
3132
return _lmAvailable.Value;
3233
}
3334

34-
public async Task<ILanguageModelCompletionResponse?> GenerateCompletionAsync(string prompt, CompletionOptions? options = null)
35+
public override async Task<ILanguageModelCompletionResponse?> GenerateCompletionAsync(string prompt, CompletionOptions? options = null)
3536
{
3637
var response = await GenerateChatCompletionAsync([new OpenAIChatCompletionMessage() { Content = prompt, Role = "user" }], options);
3738
if (response == null)
@@ -65,7 +66,7 @@ public async Task<bool> IsEnabledAsync()
6566
};
6667
}
6768

68-
public async Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(IEnumerable<ILanguageModelChatCompletionMessage> messages, CompletionOptions? options = null)
69+
public override async Task<ILanguageModelCompletionResponse?> GenerateChatCompletionAsync(IEnumerable<ILanguageModelChatCompletionMessage> messages, CompletionOptions? options = null)
6970
{
7071
using var scope = _logger.BeginScope(nameof(OpenAILanguageModelClient));
7172

@@ -112,6 +113,15 @@ public async Task<bool> IsEnabledAsync()
112113
}
113114
}
114115

116+
protected override IEnumerable<ILanguageModelChatCompletionMessage> ConvertMessages(ChatMessage[] messages)
117+
{
118+
return messages.Select(m => new OpenAIChatCompletionMessage
119+
{
120+
Role = m.Role.Value,
121+
Content = m.Text
122+
});
123+
}
124+
115125
private async Task<bool> IsEnabledInternalAsync()
116126
{
117127
using var scope = _logger.BeginScope(nameof(OpenAILanguageModelClient));

DevProxy.Abstractions/packages.lock.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
"System.Text.Json": "9.0.4"
1919
}
2020
},
21+
"Microsoft.Extensions.AI.Abstractions": {
22+
"type": "Direct",
23+
"requested": "[9.6.0, )",
24+
"resolved": "9.6.0",
25+
"contentHash": "xGO7rHg3qK8jRdriAxIrsH4voNemCf8GVmgdcPXI5gpZ6lZWqOEM4ZO8yfYxUmg7+URw2AY1h7Uc/H17g7X1Kw=="
26+
},
2127
"Microsoft.Extensions.Configuration": {
2228
"type": "Direct",
2329
"requested": "[9.0.4, )",
@@ -77,6 +83,20 @@
7783
"Newtonsoft.Json": "13.0.3"
7884
}
7985
},
86+
"Prompty.Core": {
87+
"type": "Direct",
88+
"requested": "[0.2.2-beta, )",
89+
"resolved": "0.2.2-beta",
90+
"contentHash": "OMAzLsdmrlBaw19lhZLe8VM9xULekA68sRhNZYnlRU/tMnnkhp6U8y3WZ/81yM4mLEUCHEMdy3BGE/bpfFVE/g==",
91+
"dependencies": {
92+
"Microsoft.Extensions.AI.Abstractions": "9.4.0-preview.1.25207.5",
93+
"Microsoft.Extensions.Configuration": "8.0.0",
94+
"Microsoft.Extensions.Configuration.Json": "8.0.0",
95+
"Scriban": "5.12.1",
96+
"Stubble.Core": "1.10.8",
97+
"YamlDotNet": "15.3.0"
98+
}
99+
},
80100
"System.CommandLine": {
81101
"type": "Direct",
82102
"requested": "[2.0.0-beta4.22272.1, )",
@@ -99,6 +119,11 @@
99119
"resolved": "2.4.0",
100120
"contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ=="
101121
},
122+
"Microsoft.CSharp": {
123+
"type": "Transitive",
124+
"resolved": "4.7.0",
125+
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
126+
},
102127
"Microsoft.Data.Sqlite.Core": {
103128
"type": "Transitive",
104129
"resolved": "9.0.4",
@@ -269,6 +294,11 @@
269294
"resolved": "13.0.3",
270295
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
271296
},
297+
"Scriban": {
298+
"type": "Transitive",
299+
"resolved": "5.12.1",
300+
"contentHash": "zezB4VyYSALWvveki0IAdqxrx2r0wHqwQqP5LldFSHZ3U12YZCvt1nwJb6TZVLkerHZLP2FJIkemubLfOihdBQ=="
301+
},
272302
"SharpYaml": {
273303
"type": "Transitive",
274304
"resolved": "2.1.1",
@@ -304,6 +334,21 @@
304334
"SQLitePCLRaw.core": "2.1.10"
305335
}
306336
},
337+
"Stubble.Core": {
338+
"type": "Transitive",
339+
"resolved": "1.10.8",
340+
"contentHash": "M7pXv3xz3TwhR8PJwieVncotjdC0w8AhviKPpGn2/DHlSNuTKTQdA5Ngmu3datOoeI2jXYEi3fhgncM7UueTWw==",
341+
"dependencies": {
342+
"Microsoft.CSharp": "4.7.0",
343+
"System.Collections.Immutable": "5.0.0",
344+
"System.Threading.Tasks.Extensions": "4.5.4"
345+
}
346+
},
347+
"System.Collections.Immutable": {
348+
"type": "Transitive",
349+
"resolved": "5.0.0",
350+
"contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g=="
351+
},
307352
"System.Memory": {
308353
"type": "Transitive",
309354
"resolved": "4.5.3",
@@ -318,6 +363,16 @@
318363
"type": "Transitive",
319364
"resolved": "9.0.4",
320365
"contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g=="
366+
},
367+
"System.Threading.Tasks.Extensions": {
368+
"type": "Transitive",
369+
"resolved": "4.5.4",
370+
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
371+
},
372+
"YamlDotNet": {
373+
"type": "Transitive",
374+
"resolved": "15.3.0",
375+
"contentHash": "F93japYa9YrJ59AZGhgdaUGHN7ITJ55FBBg/D/8C0BDgahv/rQD6MOSwHxOJJpon1kYyslVbeBrQ2wcJhox01w=="
321376
}
322377
}
323378
}

0 commit comments

Comments
 (0)