Skip to content

Commit d65774c

Browse files
Adds LanguageModelFailurePlugin. Closes #1313 (#1314)
1 parent b07d16b commit d65774c

27 files changed

+644
-167
lines changed

DevProxy.Abstractions/DevProxy.Abstractions.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@
1313
</PropertyGroup>
1414

1515
<ItemGroup>
16+
<PackageReference Include="Markdig" Version="0.41.3" />
1617
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
17-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
1818
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
1919
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
2020
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
2121
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
2222
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
2323
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.1" />
24-
<PackageReference Include="Prompty.Core" Version="0.2.2-beta" />
24+
<PackageReference Include="Scriban" Version="6.2.1" />
2525
<PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" />
2626
<PackageReference Include="Unobtanium.Web.Proxy" Version="0.1.5" />
27+
<PackageReference Include="YamlDotNet" Version="16.3.0" />
2728
</ItemGroup>
2829

2930
</Project>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// from: https://khalidabuhakmeh.com/parse-markdown-front-matter-with-csharp
2+
3+
using Markdig;
4+
using Markdig.Extensions.Yaml;
5+
using Markdig.Syntax;
6+
using YamlDotNet.Serialization;
7+
using YamlDotNet.Serialization.NamingConventions;
8+
9+
#pragma warning disable IDE0130
10+
namespace System;
11+
#pragma warning restore IDE0130
12+
13+
public static class MarkdownExtensions
14+
{
15+
private static readonly IDeserializer YamlDeserializer =
16+
new DeserializerBuilder()
17+
.IgnoreUnmatchedProperties()
18+
.WithNamingConvention(CamelCaseNamingConvention.Instance)
19+
.Build();
20+
21+
private static readonly MarkdownPipeline Pipeline
22+
= new MarkdownPipelineBuilder()
23+
.UseYamlFrontMatter()
24+
.Build();
25+
26+
public static (TFrontmatter? frontmatter, string? content) ParseMarkdown<TFrontmatter>(this string markdown) where TFrontmatter : new()
27+
{
28+
var document = Markdown.Parse(markdown, Pipeline);
29+
var block = document
30+
.Descendants<YamlFrontMatterBlock>()
31+
.FirstOrDefault();
32+
33+
if (block == null)
34+
{
35+
return (default, markdown);
36+
}
37+
38+
var yaml =
39+
block
40+
// this is not a mistake
41+
// we have to call .Lines 2x
42+
.Lines // StringLineGroup[]
43+
.Lines // StringLine[]
44+
.OrderByDescending(x => x.Line)
45+
.Select(x => $"{x}\n")
46+
.ToList()
47+
.Select(x => x.Replace("---", string.Empty, StringComparison.Ordinal))
48+
.Where(x => !string.IsNullOrWhiteSpace(x))
49+
.Aggregate((s, agg) => agg + s);
50+
51+
var t = YamlDeserializer.Deserialize<TFrontmatter>(yaml);
52+
var content = markdown[(block.Span.End + 1)..];
53+
return (t, content);
54+
}
55+
}

DevProxy.Abstractions/LanguageModel/BaseLanguageModelClient.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using DevProxy.Abstractions.Prompty;
56
using DevProxy.Abstractions.Utils;
6-
using Microsoft.Extensions.AI;
77
using Microsoft.Extensions.Logging;
8-
using PromptyCore = Prompty.Core;
98
using System.Collections.Concurrent;
109

1110
namespace DevProxy.Abstractions.LanguageModel;
@@ -105,7 +104,7 @@ public async Task<bool> IsEnabledAsync(CancellationToken cancellationToken)
105104
Logger.LogDebug("Loading prompt file: {FilePath}", filePath);
106105
var promptContents = File.ReadAllText(filePath);
107106

108-
var prompty = PromptyCore.Prompty.Load(promptContents, []);
107+
var prompty = Prompt.FromMarkdown(promptContents);
109108
if (prompty.Prepare(parameters) is not ChatMessage[] promptyMessages ||
110109
promptyMessages.Length == 0)
111110
{

DevProxy.Abstractions/LanguageModel/LanguageModelClientFactory.cs

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

55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.DependencyInjection;
7-
using Prompty.Core;
87

98
namespace DevProxy.Abstractions.LanguageModel;
109

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

21-
InvokerFactory.AutoDiscovery();
22-
2320
return config.Client switch
2421
{
2522
LanguageModelClient.Ollama => ActivatorUtilities.CreateInstance<OllamaLanguageModelClient>(serviceProvider, config),

DevProxy.Abstractions/LanguageModel/OllamaLanguageModelClient.cs

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

55
using System.Diagnostics;
66
using System.Net.Http.Json;
7-
using Microsoft.Extensions.AI;
7+
using DevProxy.Abstractions.Prompty;
88
using Microsoft.Extensions.Logging;
99

1010
namespace DevProxy.Abstractions.LanguageModel;
@@ -83,8 +83,8 @@ protected override IEnumerable<ILanguageModelChatCompletionMessage> ConvertMessa
8383
{
8484
return messages.Select(m => new OllamaLanguageModelChatCompletionMessage
8585
{
86-
Role = m.Role.Value,
87-
Content = m.Text
86+
Role = m.Role ?? "user",
87+
Content = m.Text ?? string.Empty
8888
});
8989
}
9090

DevProxy.Abstractions/LanguageModel/OpenAILanguageModelClient.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using DevProxy.Abstractions.Prompty;
56
using DevProxy.Abstractions.Utils;
6-
using Microsoft.Extensions.AI;
77
using Microsoft.Extensions.Logging;
88
using System.Diagnostics;
99
using System.Net.Http.Json;
@@ -91,8 +91,8 @@ protected override IEnumerable<ILanguageModelChatCompletionMessage> ConvertMessa
9191
{
9292
return messages.Select(m => new OpenAIChatCompletionMessage
9393
{
94-
Role = m.Role.Value,
95-
Content = m.Text
94+
Role = m.Role ?? "user",
95+
Content = m.Text ?? string.Empty
9696
});
9797
}
9898

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using Scriban;
2+
using YamlDotNet.Serialization;
3+
4+
namespace DevProxy.Abstractions.Prompty;
5+
6+
public class ChatMessage
7+
{
8+
public string? Role { get; set; }
9+
public string? Text { get; set; }
10+
}
11+
12+
public class ModelConfiguration
13+
{
14+
public string? Api { get; set; }
15+
[YamlMember(Alias = "parameters")]
16+
#pragma warning disable CA2227 // we need this for deserialization
17+
public Dictionary<string, object>? Options { get; set; }
18+
#pragma warning restore CA2227
19+
}
20+
21+
public class Prompt
22+
{
23+
public IEnumerable<string>? Authors { get; set; }
24+
public string? Description { get; set; }
25+
public IEnumerable<ChatMessage>? Messages { get; set; }
26+
public ModelConfiguration? Model { get; set; }
27+
public string? Name { get; set; }
28+
#pragma warning disable CA2227 // we need this for deserialization
29+
public Dictionary<string, object>? Sample { get; set; }
30+
#pragma warning restore CA2227
31+
32+
public static Prompt FromMarkdown(string markdown)
33+
{
34+
var (prompt, content) = markdown.ParseMarkdown<Prompt>();
35+
prompt ??= new();
36+
37+
if (content is not null)
38+
{
39+
prompt.Messages = GetMessages(content);
40+
}
41+
42+
return prompt;
43+
}
44+
45+
public IEnumerable<ChatMessage> Prepare(Dictionary<string, object>? inputs, bool mergeSample = false)
46+
{
47+
inputs ??= [];
48+
49+
if (mergeSample && Sample is not null)
50+
{
51+
foreach (var kvp in Sample)
52+
{
53+
inputs[kvp.Key] = kvp.Value;
54+
}
55+
}
56+
57+
var messages = new List<ChatMessage>();
58+
59+
foreach (var message in Messages ?? [])
60+
{
61+
if (message.Text is null)
62+
{
63+
continue;
64+
}
65+
66+
var template = Template.Parse(message.Text);
67+
messages.Add(new()
68+
{
69+
Role = message.Role,
70+
Text = template.Render(inputs)
71+
});
72+
}
73+
74+
return messages;
75+
}
76+
77+
private static List<ChatMessage> GetMessages(string markdown)
78+
{
79+
var messageTypes = new[] { "system", "user", "assistant" };
80+
var messages = new List<ChatMessage>();
81+
var lines = markdown.Split('\n', StringSplitOptions.RemoveEmptyEntries);
82+
ChatMessage? currentMessage = null;
83+
84+
foreach (var line in lines)
85+
{
86+
var trimmedLine = line.Trim();
87+
if (messageTypes.Any(type => trimmedLine.StartsWith($"{type}:", StringComparison.OrdinalIgnoreCase)))
88+
{
89+
if (currentMessage is not null)
90+
{
91+
messages.Add(currentMessage);
92+
}
93+
94+
var role = trimmedLine.Split(':')[0].Trim().ToLowerInvariant();
95+
currentMessage = new ChatMessage
96+
{
97+
Role = role,
98+
Text = string.Empty
99+
};
100+
continue;
101+
}
102+
103+
if (currentMessage is not null)
104+
{
105+
currentMessage.Text += line;
106+
}
107+
}
108+
if (currentMessage is not null)
109+
{
110+
messages.Add(currentMessage);
111+
}
112+
113+
return messages;
114+
}
115+
}

DevProxy.Abstractions/packages.lock.json

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
"version": 1,
33
"dependencies": {
44
"net9.0": {
5+
"Markdig": {
6+
"type": "Direct",
7+
"requested": "[0.41.3, )",
8+
"resolved": "0.41.3",
9+
"contentHash": "i3vSTyGpBGWbJB04aJ3cPJs0T3BV2e1nduW3EUHK/i+xUupYbym75iZPss/XjqhS5JlBErwQYnx7ofK3Zcsozg=="
10+
},
511
"Microsoft.EntityFrameworkCore.Sqlite": {
612
"type": "Direct",
713
"requested": "[9.0.4, )",
@@ -18,12 +24,6 @@
1824
"System.Text.Json": "9.0.4"
1925
}
2026
},
21-
"Microsoft.Extensions.AI.Abstractions": {
22-
"type": "Direct",
23-
"requested": "[9.6.0, )",
24-
"resolved": "9.6.0",
25-
"contentHash": "xGO7rHg3qK8jRdriAxIrsH4voNemCf8GVmgdcPXI5gpZ6lZWqOEM4ZO8yfYxUmg7+URw2AY1h7Uc/H17g7X1Kw=="
26-
},
2727
"Microsoft.Extensions.Configuration": {
2828
"type": "Direct",
2929
"requested": "[9.0.4, )",
@@ -83,19 +83,11 @@
8383
"Newtonsoft.Json": "13.0.3"
8484
}
8585
},
86-
"Prompty.Core": {
86+
"Scriban": {
8787
"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-
}
88+
"requested": "[6.2.1, )",
89+
"resolved": "6.2.1",
90+
"contentHash": "jauX7gvreKvlD1+tkQ9D1i0kNg2p3P1ZqkDftJeTB7JAF7zKtafpyKTtr3m5Kr6d4GYw0CDfRcm2P07/efwdqQ=="
9991
},
10092
"System.CommandLine": {
10193
"type": "Direct",
@@ -114,16 +106,17 @@
114106
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
115107
}
116108
},
109+
"YamlDotNet": {
110+
"type": "Direct",
111+
"requested": "[16.3.0, )",
112+
"resolved": "16.3.0",
113+
"contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA=="
114+
},
117115
"BouncyCastle.Cryptography": {
118116
"type": "Transitive",
119117
"resolved": "2.4.0",
120118
"contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ=="
121119
},
122-
"Microsoft.CSharp": {
123-
"type": "Transitive",
124-
"resolved": "4.7.0",
125-
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
126-
},
127120
"Microsoft.Data.Sqlite.Core": {
128121
"type": "Transitive",
129122
"resolved": "9.0.4",
@@ -294,11 +287,6 @@
294287
"resolved": "13.0.3",
295288
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
296289
},
297-
"Scriban": {
298-
"type": "Transitive",
299-
"resolved": "5.12.1",
300-
"contentHash": "zezB4VyYSALWvveki0IAdqxrx2r0wHqwQqP5LldFSHZ3U12YZCvt1nwJb6TZVLkerHZLP2FJIkemubLfOihdBQ=="
301-
},
302290
"SharpYaml": {
303291
"type": "Transitive",
304292
"resolved": "2.1.1",
@@ -334,21 +322,6 @@
334322
"SQLitePCLRaw.core": "2.1.10"
335323
}
336324
},
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-
},
352325
"System.Memory": {
353326
"type": "Transitive",
354327
"resolved": "4.5.3",
@@ -363,16 +336,6 @@
363336
"type": "Transitive",
364337
"resolved": "9.0.4",
365338
"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=="
376339
}
377340
}
378341
}

0 commit comments

Comments
 (0)