Skip to content

Commit cb205d5

Browse files
authored
Support .NET 9.0 and improve compatibility (#12)
* Add serialization and save methods to PromptFile and stores Implemented `ToStream` and `ToFile` methods in `PromptFile` for serializing instances to streams and files, respectively. Added corresponding tests in `PromptFileTests`. Introduced `Save` method in `FilePromptStore` and updated `IPromptStore` interface, along with unit tests to ensure proper file saving behavior. * add json schema output This commit adds the initial pass at implementing JSON Schema output formatting * Add unit tests for Output.ToSchemaDocument method Introduce a new test class `OutputTests` to verify various scenarios for the `ToSchemaDocument` method. Ensures proper handling of null, empty, and specific schema JSON inputs, improving code reliability. * Update JsonSchema.Net to version 7.3.3 Bump the JsonSchema.Net package from version 7.2.3 to 7.3.3 to include the latest fixes and improvements. No other changes were made to dependencies. * Refactor schema handling logic in Output class and tests. Simplified schema deserialization and adjusted additionalProperties handling for improved clarity and consistency. Updated tests to align with the new implementation, ensuring behavior remains accurate across scenarios. * Update output format to text and fix config consistency Changed the default output format from JSON to text in the sample prompt. Also ensured consistency between the output format in the configuration and its usage in the prompt file logic. * Update parameter type to nullable in test method Modified the `schemaJson` parameter in the `ToSchemaDocument_VariousScenarios_ReturnsExpectedResult` test method to be nullable. This ensures compatibility with scenarios where a null value is passed. * Add validation for missing prompt file name in Save method Ensure the Save method throws an ArgumentException if no valid name is provided for the prompt file. Updated the FilePromptStore tests to include a new unit test for this validation logic. This prevents saving files without a proper name, improving robustness. * Update license metadata to use SPDX expression Replaced the `PackageLicenseFile` property with `PackageLicenseExpression` for specifying the license as MIT. This improves compatibility with modern NuGet package requirements and ensures proper license recognition. * Use null-forgiving operator to resolve nullability warning Applied the null-forgiving operator to the `ToStream` method invocation to explicitly indicate that the `null` argument is intentional. This resolves a compiler nullability warning while maintaining code clarity. * Support .NET 9.0 and update package dependencies Add multi-targeting support for .NET 9.0 in both main and test projects. Update Fluid.Core to version 2.23.0 and JsonSchema.Net to version 7.3.4 for compatibility and improvements. Ensure continued functionality across supported frameworks. * Update workflows to use .NET 9.0.x Updated the .NET version in both `dotnet.yml` and `release.yaml` workflows from 8.0.x to 9.0.x. This ensures compatibility with the latest .NET features and tools. * Add output format support and Azure Table Store saving Updated the configuration to support specifying output format in a new `output` section for improved flexibility, maintaining backward compatibility. Added functionality to save prompt files to Azure Table Store and introduced a method to create `PromptEntity` instances from `PromptFile` objects. * Add tests for JSON schema handling in OpenAi options Introduce unit tests to validate JSON schema support for `ToOpenAiChatCompletionOptions`. Add a prompt sample for testing and ensure empty schema handling raises proper exceptions. * Add test for invalid output format exception in OpenAi options This test ensures that an exception is thrown when an unsupported output format is provided to the `ToOpenAiChatCompletionOptions` method. It validates proper error handling and improves robustness against invalid configurations. * Remove validation for output schema in PromptFile.cs * Add test case for trimming and cleaning extra spaces A new test case was added to ensure names with leading or trailing spaces are cleaned properly. This improves the robustness of the `FromStream_WithNamePart_CleansTheName` method. * Add test for consistent Output.ToSchemaDocument results Ensure `Output.ToSchemaDocument` consistently returns the same result when called multiple times. Removed unnecessary null coalescing in the method to improve clarity and avoid redundant defaults. * Add test for handling empty Output in OpenAi options conversion This commit introduces a new test to verify that the `ToOpenAiChatCompletionOptions` method throws a `DotPromptException` when the Output configuration is empty. This ensures proper validation and error handling for invalid schema configurations in the OpenAi extensions. * Add TODO for scanning schema document for additionalProperties
1 parent b602bb0 commit cb205d5

18 files changed

Lines changed: 512 additions & 11 deletions

.github/workflows/dotnet.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Setup .NET
2121
uses: actions/setup-dotnet@v4
2222
with:
23-
dotnet-version: 8.0.x
23+
dotnet-version: 9.0.x
2424
- name: Restore dependencies
2525
run: dotnet restore
2626
- name: Build

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: Setup .NET
1515
uses: actions/setup-dotnet@v4
1616
with:
17-
dotnet-version: 8.0.x
17+
dotnet-version: 9.0.x
1818
- name: Get latest tag version
1919
id: vars
2020
run: echo "tag=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_OUTPUT

DotPrompt.Tests/DotPrompt.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

DotPrompt.Tests/Extensions/OpenAi/OpenAiExtensionsTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,67 @@ public void ToOpenAiChatCompletionOptions_WithMissingConfig_ReturnsAValidOptions
9696

9797
Assert.Equivalent(ChatResponseFormat.CreateJsonObjectFormat(), options.ResponseFormat);
9898
}
99+
100+
[Fact]
101+
public void ToOpenAiChatCompletionOptions_WithJsonSchemaFormat_ReturnsAValidOptionsInstance()
102+
{
103+
var promptFile = PromptFile.FromFile("SamplePrompts/basic-json-format.prompt");
104+
var options = promptFile.ToOpenAiChatCompletionOptions();
105+
106+
const string expectedSchema = """{"type":"object","required":["field1"],"properties":{"field1":{"type":"string","description":"An example description for the field"},"field2":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}""";
107+
108+
var jsonSchemaProperty = options.ResponseFormat.GetType().GetProperty("JsonSchema");
109+
var jsonSchemaValue = jsonSchemaProperty!.GetValue(options.ResponseFormat);
110+
111+
var schemaProperty = jsonSchemaValue!.GetType().GetProperty("Schema");
112+
var schemaValue = schemaProperty!.GetValue(jsonSchemaValue) as BinaryData;
113+
114+
var optionsSchema = schemaValue!.ToString();
115+
116+
Assert.Equal(expectedSchema, optionsSchema);
117+
118+
Assert.Equivalent(ChatResponseFormat.CreateJsonObjectFormat(), options.ResponseFormat);
119+
}
120+
121+
[Fact]
122+
public void ToOpenAiChatCompletionOptions_WithEmptySchema_ThrowsAnException()
123+
{
124+
var promptFile = PromptFile.FromFile("SamplePrompts/basic-json-format.prompt");
125+
promptFile.Config.Output!.Schema = null;
126+
127+
var action = () => promptFile.ToOpenAiChatCompletionOptions();
128+
129+
var exception = Assert.Throws<DotPromptException>(action);
130+
Assert.Contains("A valid schema was not provided to be used with the JsonSchema response type", exception.Message);
131+
}
132+
133+
[Fact]
134+
public void ToOpenAiChatCompletionOptions_WithEmptyOutput_ThrowsAnException()
135+
{
136+
var promptFile = PromptFile.FromFile("SamplePrompts/basic-json-format.prompt");
137+
promptFile.Config.Output = null;
138+
139+
var action = () => promptFile.ToOpenAiChatCompletionOptions();
140+
141+
var exception = Assert.Throws<DotPromptException>(action);
142+
Assert.Contains("A valid schema was not provided to be used with the JsonSchema response type", exception.Message);
143+
}
144+
145+
[Fact]
146+
public void ToOpenAiChatCompletionOptions_WithInvalidFormat_ThrowsAnException()
147+
{
148+
// Arrange
149+
var promptFileMock = new PromptFile
150+
{
151+
Name = "test",
152+
Config = new PromptConfig
153+
{
154+
OutputFormat = (OutputFormat)999
155+
}
156+
};
157+
158+
// Act & Assert
159+
var exception = Assert.Throws<DotPromptException>(() => promptFileMock.ToOpenAiChatCompletionOptions());
160+
Assert.Contains("The requested output format is not available", exception.Message);
161+
}
99162
}

DotPrompt.Tests/FilePromptStoreTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,74 @@ public void Load_WhenCalledOnDirectoryWithSubDirectories_RetrievesAllPromptFiles
4747

4848
Assert.Equal(2, promptFiles.Count);
4949
}
50+
51+
[Fact]
52+
public void Save_WhenCalledWithValidPromptFile_SavesToFile()
53+
{
54+
using var tempDirectory = new TempDirectory();
55+
var originalPromptFile = PromptFile.FromFile("SamplePrompts/basic.prompt");
56+
57+
var promptStore = new FilePromptStore(tempDirectory.TempPath);
58+
59+
var newFilePath = tempDirectory.GetTempFilePath();
60+
var newFileName = Path.GetFileName(newFilePath);
61+
promptStore.Save(originalPromptFile, newFileName);
62+
63+
var savedPromptFile = PromptFile.FromFile(newFilePath);
64+
65+
Assert.Equivalent(originalPromptFile, savedPromptFile, true);
66+
}
67+
68+
[Fact]
69+
public void Save_WhenCalledWithMissingNameValue_ValueFromPromptIsUsed()
70+
{
71+
using var tempDirectory = new TempDirectory();
72+
var originalPromptFile = PromptFile.FromFile("SamplePrompts/basic.prompt");
73+
originalPromptFile.Name = "missing-name-test";
74+
75+
var promptStore = new FilePromptStore(tempDirectory.TempPath);
76+
promptStore.Save(originalPromptFile);
77+
78+
var tempFiles = Directory.EnumerateFiles(tempDirectory.TempPath, "*.prompt")
79+
.Select(Path.GetFileName)
80+
.ToList();
81+
82+
Assert.Contains("missing-name-test.prompt", tempFiles);
83+
}
84+
85+
[Fact]
86+
public void Save_WhenCalledWithNoValidNameValue_ThrowsException()
87+
{
88+
using var tempDirectory = new TempDirectory();
89+
var originalPromptFile = PromptFile.FromFile("SamplePrompts/basic.prompt");
90+
originalPromptFile.Name = string.Empty;
91+
92+
var promptStore = new FilePromptStore(tempDirectory.TempPath);
93+
var act = () => promptStore.Save(originalPromptFile);
94+
95+
var exception = Assert.Throws<ArgumentException>(act);
96+
Assert.Contains("A name must be provided for the prompt file", exception.Message);
97+
}
98+
}
99+
100+
public class TempDirectory : IDisposable
101+
{
102+
private readonly DirectoryInfo _path = Directory.CreateTempSubdirectory("prompts_");
103+
104+
public string TempPath => _path.FullName;
105+
106+
public string GetTempFilePath()
107+
{
108+
return Path.Join(_path.FullName, $"{Path.GetRandomFileName()}.prompt");
109+
}
110+
111+
public void Dispose()
112+
{
113+
if (_path.Exists)
114+
{
115+
_path.Delete(true);
116+
}
117+
118+
GC.SuppressFinalize(this);
119+
}
50120
}

DotPrompt.Tests/OutputTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Text.Json;
2+
3+
namespace DotPrompt.Tests;
4+
5+
public class OutputTests
6+
{
7+
[Theory]
8+
[InlineData(null, "")]
9+
[InlineData("{}", "{\"additionalProperties\":false}")]
10+
[InlineData("{\"additionalProperties\":true}", "{\"additionalProperties\":true}")]
11+
public void ToSchemaDocument_VariousScenarios_ReturnsExpectedResult(string? schemaJson, string expectedJson)
12+
{
13+
// Arrange
14+
var output = new Output();
15+
16+
if (schemaJson != null)
17+
{
18+
var schema = JsonDocument.Parse(schemaJson);
19+
output.Schema = schema.Deserialize<Dictionary<string, object>>();
20+
}
21+
22+
// Act
23+
var result = output.ToSchemaDocument();
24+
25+
// Assert
26+
Assert.Equal(expectedJson, result);
27+
}
28+
29+
[Fact]
30+
public void ToSchemaDocument_WhenCalledTwice_ReturnsSameResult()
31+
{
32+
const string schemaJson = """{"additionalProperties":true}""";
33+
var output = new Output
34+
{
35+
Schema = JsonSerializer.Deserialize<Dictionary<string, object>>(schemaJson)
36+
};
37+
38+
var result1 = output.ToSchemaDocument();
39+
var result2 = output.ToSchemaDocument();
40+
41+
Assert.Equal(result1, result2);
42+
}
43+
}

DotPrompt.Tests/PromptFileTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ public void FromStream_WithUnreadableStream_ThrowsException()
175175
[InlineData("do-not-clean", "do-not-clean")]
176176
[InlineData("My COOL nAMe", "my-cool-name")]
177177
[InlineData("this <is .pretty> un*cl()ean", "this-is-pretty-unclean")]
178+
[InlineData(" some extra space ", "some-extra-space")]
178179
public void FromStream_WithNamePart_CleansTheName(string inputName, string expectedName)
179180
{
180181
const string content = "prompts:\n system: System prompt\n user: User prompt";
@@ -342,6 +343,71 @@ public void GenerateUserPrompt_WithNumericValueTypes_GeneratesValidPrompt(object
342343
Assert.Equal(expectedPrompt, userPrompt);
343344
}
344345

346+
[Fact]
347+
public void ToStream_WithValidInput_ProducesExpectedContent()
348+
{
349+
var promptFile = new PromptFile
350+
{
351+
Name = "test",
352+
Config = new PromptConfig
353+
{
354+
MaxTokens = 500,
355+
OutputFormat = OutputFormat.Json,
356+
Input = new InputSchema
357+
{
358+
Parameters = new Dictionary<string, string>
359+
{
360+
{ "test?", "string" }
361+
}
362+
}
363+
},
364+
Prompts = new Prompts
365+
{
366+
System = "system prompt",
367+
User = "user prompt"
368+
}
369+
};
370+
371+
var expected = "name: test\nconfig:\n input:\n parameters:\n test?: string\n outputFormat: Json\n maxTokens: 500\nprompts:\n system: system prompt\n user: user prompt\n";
372+
373+
var ms = new MemoryStream();
374+
promptFile.ToStream(ms);
375+
376+
var actual = Encoding.UTF8.GetString(ms.ToArray());
377+
378+
Assert.Equal(expected, actual);
379+
}
380+
381+
[Fact]
382+
public void ToStream_WithNoStreamArgument_ThrowsError()
383+
{
384+
var promptFile = new PromptFile
385+
{
386+
Name = "test"
387+
};
388+
389+
var act = () => promptFile.ToStream(null!);
390+
391+
Assert.Throws<ArgumentNullException>(act);
392+
}
393+
394+
[Fact]
395+
public void ToStream_WithNonWriteableStream_ThrowsError()
396+
{
397+
var promptFile = new PromptFile
398+
{
399+
Name = "test"
400+
};
401+
402+
var ms = new MemoryStream();
403+
var stream = new MockStream(ms, true, false);
404+
405+
var act = () => promptFile.ToStream(stream);
406+
407+
var exception = Assert.Throws<DotPromptException>(act);
408+
Assert.Contains("Unable to use stream as it is not writeable", exception.Message);
409+
}
410+
345411
public static IEnumerable<object[]> NumericData =>
346412
new List<object[]>
347413
{
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
model: claude-3-5-sonnet-latest
2+
config:
3+
temperature: 0.9
4+
maxTokens: 500
5+
input:
6+
parameters:
7+
country: string
8+
style?: string
9+
default:
10+
country: Malta
11+
output:
12+
format: jsonSchema
13+
schema:
14+
type: object
15+
required:
16+
- field1
17+
properties:
18+
field1:
19+
type: string
20+
description: An example description for the field
21+
field2:
22+
type: array
23+
items:
24+
type: string
25+
prompts:
26+
system: |
27+
You are a helpful AI assistant that enjoys making penguin related puns. You should work as many into your response as possible
28+
user: |
29+
I am looking at going on holiday to {{ country }} and would like to know more about it, what can you tell me?
30+
{% if style -%}
31+
Can you answer in the style of a {{ style }}
32+
{% endif -%}

DotPrompt.Tests/SamplePrompts/basic.prompt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ config:
99
style?: string
1010
default:
1111
country: Malta
12+
output:
13+
format: text
14+
schema:
15+
type: object
16+
required:
17+
- field1
18+
properties:
19+
field1:
20+
type: string
21+
description: An example description for the field
22+
field2:
23+
type: array
24+
items:
25+
type: string
1226
prompts:
1327
system: |
1428
You are a helpful AI assistant that enjoys making penguin related puns. You should work as many into your response as possible

DotPrompt/DotPrompt.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<PackageId>DotPrompt</PackageId>
@@ -14,14 +14,15 @@
1414
<RepositoryUrl>https://github.com/elastacloud/DotPrompt</RepositoryUrl>
1515
<PackageOutputPath>./nupkg</PackageOutputPath>
1616
<PackageReadmeFile>README.md</PackageReadmeFile>
17-
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
17+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1818
<LangVersion>default</LangVersion>
1919
<NoWarn>NU1702</NoWarn>
2020
<GenerateDocumentationFile>true</GenerateDocumentationFile>
2121
</PropertyGroup>
2222

2323
<ItemGroup>
24-
<PackageReference Include="Fluid.Core" Version="2.19.0" />
24+
<PackageReference Include="Fluid.Core" Version="2.23.0" />
25+
<PackageReference Include="JsonSchema.Net" Version="7.3.4" />
2526
<PackageReference Include="OpenAI" Version="2.1.0" />
2627
<PackageReference Include="YamlDotNet" Version="16.3.0" />
2728
</ItemGroup>

0 commit comments

Comments
 (0)