From aa93512a325ffbbca2600b24e83d0d6c67e9ee1a Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Thu, 13 Mar 2025 08:40:50 -0700 Subject: [PATCH 01/11] add public methods for Load(Stream...) and LoadAsync(Stream,...), includes tests --- .../promptycs/Prompty.Core.Tests/LoadTests.cs | 28 +++++++++++++++ runtime/promptycs/Prompty.Core/Prompty.cs | 36 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs b/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs index e5fdd14e..03d46f5f 100644 --- a/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs +++ b/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs @@ -36,6 +36,34 @@ public void LoadRawWithConfig(string path) Assert.Equal("FAKE_TYPE", prompty.Model?.Configuration.Type); } + /// + /// Test the Loading from a Stream + /// + [Fact] + public void LoadStream() + { + var path = "prompty/basic.prompty"; + using var stream = File.OpenRead(path); + var prompty = Prompty.Load(stream); + + Assert.NotNull(prompty); + Assert.NotNull(prompty.Content); + } + + /// + /// Test the Loading from a Stream Async + /// + [Fact] + public async Task LoadStreamAsync() + { + var path = "prompty/basic.prompty"; + using var stream = File.OpenRead(path); + var prompty = await Prompty.LoadAsync(stream); + + Assert.NotNull(prompty); + Assert.NotNull(prompty.Content); + } + [Fact] public void BasicSampleParameters() { diff --git a/runtime/promptycs/Prompty.Core/Prompty.cs b/runtime/promptycs/Prompty.Core/Prompty.cs index eccd0912..4c1e2b91 100644 --- a/runtime/promptycs/Prompty.Core/Prompty.cs +++ b/runtime/promptycs/Prompty.Core/Prompty.cs @@ -181,6 +181,42 @@ public static async Task LoadAsync(string path, string configuration = return prompty; } + /// + /// Load a prompty file from a Stream + /// + /// Stream to read the prompty file from. + /// Id of the configuration to use. + public static Prompty Load(Stream stream, string configuration = "default") + { + using var reader = new StreamReader(stream); + string text = reader.ReadToEnd(); + + var global_config = GlobalConfig.Load(System.IO.Path.GetDirectoryName(stream.ToString()) ?? string.Empty, configuration) ?? []; + global_config = Normalizer.Normalize(global_config, stream.ToString()); + + var frontmatter = LoadRaw(text, global_config, stream.ToString()); + var prompty = Convert(frontmatter, stream.ToString()); + return prompty; + } + + /// + /// Load a prompty file from a Stream Asynchronously + /// + /// Stream to read the prompty file from. + /// Id of the configuration to use. + public static async Task LoadAsync(Stream stream, string configuration = "default") + { + using var reader = new StreamReader(stream); + string text = await reader.ReadToEndAsync(); + + var global_config = await GlobalConfig.LoadAsync(System.IO.Path.GetDirectoryName(stream.ToString()) ?? string.Empty, configuration) ?? []; + global_config = Normalizer.Normalize(global_config, stream.ToString()); + + var frontmatter = LoadRaw(text, global_config, stream.ToString()); + var prompty = Convert(frontmatter, stream.ToString()); + return prompty; + } + /// /// Load a prompty file using the provided text content. /// From 077be6f3eab26f8d4cc919a428d89d5edffeb2ec Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Thu, 13 Mar 2025 09:45:16 -0700 Subject: [PATCH 02/11] adds a PromptyAttribute that allows for specifying prompty files as attributes on a class or method --- .../Prompty.Core.Tests.csproj | 2 + .../PromptyAttributeTests.cs | 58 ++++++++++++++ .../Prompty.Core/PromptyAttribute.cs | 80 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs create mode 100644 runtime/promptycs/Prompty.Core/PromptyAttribute.cs diff --git a/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj b/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj index bb5a5b8b..d8448940 100644 --- a/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj +++ b/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj @@ -69,6 +69,8 @@ Always + + Always diff --git a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs new file mode 100644 index 00000000..9f36a080 --- /dev/null +++ b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs @@ -0,0 +1,58 @@ +namespace Prompty.Core.Tests; + + +[Prompty("prompty/basic.prompty")] +public class ClassWithAttribute { } + +[Prompty("prompty/basic.prompty", IsResource = true)] +public class ClassWithResourceAttribute { } + +public class PromptyAttributeTests +{ + public PromptyAttributeTests() + { + Environment.SetEnvironmentVariable("AZURE_OPENAI_ENDPOINT", "ENDPOINT_VALUE"); + } + + [Fact] + public void LoadFromFile() + { + var attr = (PromptyAttribute)Attribute.GetCustomAttribute( + typeof(ClassWithAttribute), + typeof(PromptyAttribute))!; + + Assert.NotNull(attr); + Assert.Equal("prompty/basic.prompty", attr.File); + Assert.False(attr.IsResource); + Assert.NotNull(attr.Prompt); + Assert.NotNull(attr.Messages); + } + + [Fact] + public void LoadFromResource() + { + var attr = (PromptyAttribute)Attribute.GetCustomAttribute( + typeof(ClassWithResourceAttribute), + typeof(PromptyAttribute))!; + + Assert.NotNull(attr); + Assert.Equal("prompty/basic.prompty", attr.File); + Assert.True(attr.IsResource); + Assert.NotNull(attr.Prompt); + Assert.NotNull(attr.Messages); + } + + [Fact] + public void ThrowsOnInvalidFile() + { + Assert.Throws(() => + new PromptyAttribute("nonexistent.prompty", false)); + } + + [Fact] + public void ThrowsOnInvalidResource() + { + Assert.Throws(() => + new PromptyAttribute("nonexistent.prompty", true)); + } +} diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs new file mode 100644 index 00000000..7d3173f7 --- /dev/null +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.AI; +using System.Reflection; + +namespace Prompty.Core; + +/// +/// Prompty Attribute - used to load a prompty file or resource from an attribute +/// +/// +/// [Prompty("prompty/basic.prompty"] +/// [Prompty("prompty/embedded-resource-path.prompty", IsResource = true)] +/// public class MyClass +/// {...} +/// in a class or method then use the attribute to load the prompty +/// ... +/// var prompty = (PromptyAttribute)Attribute.GetCustomAttribute(typeof(MyClass), typeof(PromptyAttribute)); +/// var messages = prompty.Messages; +/// ... +/// +public class PromptyAttribute : Attribute +{ + /// + /// The file name of the prompty file + public string File { get; set; } + + /// + /// Is the file a resource + /// + public bool IsResource { get; set; } = false; + + /// + /// The configuration id to use + /// + public string? Configuration { get; set; } + + /// + /// The parameters for input + /// + public string[]? Params { get; set; } + + /// + /// the loaded prompty + /// + public Prompty Prompt { get; set; } + + /// + /// The prepared messages + /// + public ChatMessage[] Messages => (ChatMessage[])Prompt.Prepare(); + + public PromptyAttribute(string File, bool IsResource = false, string Configuration = "default", string[] Params = null!) + { + this.File = File; + this.IsResource = IsResource; + this.Configuration = Configuration; + this.Params = Params; + + if (IsResource == true) + { + // get the stream from the resource name + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(File); + if (stream == null) + { + throw new FileNotFoundException($"Resource {File} not found"); + } + this.Prompt = Prompty.Load(stream, Configuration); + } + else + { + if (!System.IO.File.Exists(File)) + { + throw new FileNotFoundException($"File {File} not found"); + } + // load the file + this.Prompt = Prompty.Load(File, Configuration); + } + + } +} From cfa4007328abb6b3dcc628431b0f18a9d271221d Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Thu, 13 Mar 2025 11:54:55 -0700 Subject: [PATCH 03/11] add requested tests --- .../promptycs/Prompty.Core.Tests/LoadTests.cs | 43 +++++++++++++++++++ .../Prompty.Core.Tests.csproj | 1 + 2 files changed, 44 insertions(+) diff --git a/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs b/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs index 03d46f5f..e296a7a2 100644 --- a/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs +++ b/runtime/promptycs/Prompty.Core.Tests/LoadTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Transactions; namespace Prompty.Core.Tests; @@ -50,6 +51,48 @@ public void LoadStream() Assert.NotNull(prompty.Content); } + /// + /// Test the Loading from an embedded resource + /// + [Fact] + public void LoadEmbeddedResource() + { + // Get the fully qualified name of the embedded resource + var assembly = Assembly.GetExecutingAssembly(); + var resourceNames = assembly.GetManifestResourceNames(); + var resourceName = resourceNames.FirstOrDefault(r => r.EndsWith("basic.prompty")); + + Assert.NotNull(resourceName); // Ensure we found the resource + + using var stream = assembly.GetManifestResourceStream(resourceName); + Assert.NotNull(stream); + var prompty = Prompty.Load(stream!); + + Assert.NotNull(prompty); + Assert.NotNull(prompty.Content); + } + + /// + /// Test the Loading from an embedded resource with config + /// + [Fact] + public void LoadEmbeddedResourceWithConfig() + { + // Get the fully qualified name of the embedded resource + var assembly = Assembly.GetExecutingAssembly(); + var resourceNames = assembly.GetManifestResourceNames(); + var resourceName = resourceNames.FirstOrDefault(r => r.EndsWith("basic.prompty")); + + Assert.NotNull(resourceName); // Ensure we found the resource + + using var stream = assembly.GetManifestResourceStream(resourceName); + Assert.NotNull(stream); + var prompty = Prompty.Load(stream!, "fake"); + + Assert.Equal("FAKE_TYPE", prompty.Model?.Configuration.Type); + } + + /// /// Test the Loading from a Stream Async /// diff --git a/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj b/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj index bb5a5b8b..e6a0505d 100644 --- a/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj +++ b/runtime/promptycs/Prompty.Core.Tests/Prompty.Core.Tests.csproj @@ -69,6 +69,7 @@ Always + Always From fbd8bb5c4a48816690145e92d0ef9ee3998308b3 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Thu, 13 Mar 2025 13:49:14 -0700 Subject: [PATCH 04/11] changes to handle params and update tests --- .../PromptyAttributeTests.cs | 45 +++++++++++++++++++ .../Prompty.Core/PromptyAttribute.cs | 23 +++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs index 9f36a080..e24a1b57 100644 --- a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs +++ b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.AI; + namespace Prompty.Core.Tests; @@ -7,6 +9,9 @@ public class ClassWithAttribute { } [Prompty("prompty/basic.prompty", IsResource = true)] public class ClassWithResourceAttribute { } +[Prompty("prompty/basic.prompty", IsResource = true, Configuration = "FAKE_TYPE", Params = new string[] { "firstName", "Caspar", "lastName", "Haglund", "question", "What is your name?" })] +public class ClassWithResourceAttributeAndCofigAndParams { } + public class PromptyAttributeTests { public PromptyAttributeTests() @@ -14,6 +19,9 @@ public PromptyAttributeTests() Environment.SetEnvironmentVariable("AZURE_OPENAI_ENDPOINT", "ENDPOINT_VALUE"); } + /// + /// Test Loading from a File path + /// [Fact] public void LoadFromFile() { @@ -28,6 +36,9 @@ public void LoadFromFile() Assert.NotNull(attr.Messages); } + /// + /// Test Loading from an embedded Resource path + /// [Fact] public void LoadFromResource() { @@ -55,4 +66,38 @@ public void ThrowsOnInvalidResource() Assert.Throws(() => new PromptyAttribute("nonexistent.prompty", true)); } + + /// + /// Test Loading from a Resource path with configuration and parameters + /// + [Fact] + public void LoadFromResourceWithConfigAndParams() + { + + var attr = (PromptyAttribute)Attribute.GetCustomAttribute( + typeof(ClassWithResourceAttributeAndCofigAndParams), + typeof(PromptyAttribute))!; + + var messages = attr.Messages; + + Assert.NotNull(attr); + Assert.Equal("prompty/basic.prompty", attr.File); + Assert.True(attr.IsResource); + Assert.Equal("FAKE_TYPE", attr.Configuration); + Assert.NotNull(attr.Params); + Assert.Equal(6, attr.Params.Length); + Assert.Equal("firstName", attr.Params[0]); + Assert.Equal("Caspar", attr.Params[1]); + Assert.Equal("lastName", attr.Params[2]); + Assert.Equal("Haglund", attr.Params[3]); + Assert.Equal("question", attr.Params[4]); + Assert.Equal("What is your name?", attr.Params[5]); + Assert.NotNull(attr.Prompt); + Assert.IsType(messages); + Assert.NotNull(messages); + Assert.Equal(2, messages.Length); + Assert.Contains("Caspar", messages[0].Text); + Assert.Contains("Haglund", messages[0].Text); + Assert.Contains("What is your name?", messages[1].Text); + } } diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index 7d3173f7..dd151538 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -8,7 +8,7 @@ namespace Prompty.Core; /// /// /// [Prompty("prompty/basic.prompty"] -/// [Prompty("prompty/embedded-resource-path.prompty", IsResource = true)] +/// [Prompty("prompty/embedded-resource-path.prompty", IsResource = true, Configuration = "FAKE_TYPE", Params = new string[] { "question", "answer" })] /// public class MyClass /// {...} /// in a class or method then use the attribute to load the prompty @@ -46,7 +46,7 @@ public class PromptyAttribute : Attribute /// /// The prepared messages /// - public ChatMessage[] Messages => (ChatMessage[])Prompt.Prepare(); + public ChatMessage[] Messages => (ChatMessage[])Prompt.Prepare(GetParams(), mergeSample: true); public PromptyAttribute(string File, bool IsResource = false, string Configuration = "default", string[] Params = null!) { @@ -55,6 +55,8 @@ public PromptyAttribute(string File, bool IsResource = false, string Configurati this.Configuration = Configuration; this.Params = Params; + InvokerFactory.AutoDiscovery(); + if (IsResource == true) { // get the stream from the resource name @@ -75,6 +77,23 @@ public PromptyAttribute(string File, bool IsResource = false, string Configurati // load the file this.Prompt = Prompty.Load(File, Configuration); } + } + /// + /// convert the params to a dictionary + /// + /// Dictionary + public Dictionary GetParams() + { + var dict = new Dictionary(); + if (Params != null) + { + for (int i = 0; i < Params.Length; i += 2) + { + if (i + 1 < Params.Length) + dict.Add(Params[i], Params[i + 1]); + } + } + return dict; } } From 058e284c13cbb9fb34dfc93f9050109b8ad8d5ab Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Thu, 13 Mar 2025 14:03:56 -0700 Subject: [PATCH 05/11] add tests for multiple attributes --- .../PromptyAttributeTests.cs | 68 +++++++++++++++++++ .../Prompty.Core/PromptyAttribute.cs | 1 + 2 files changed, 69 insertions(+) diff --git a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs index e24a1b57..f2b90ab5 100644 --- a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs +++ b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs @@ -12,6 +12,14 @@ public class ClassWithResourceAttribute { } [Prompty("prompty/basic.prompty", IsResource = true, Configuration = "FAKE_TYPE", Params = new string[] { "firstName", "Caspar", "lastName", "Haglund", "question", "What is your name?" })] public class ClassWithResourceAttributeAndCofigAndParams { } +[Prompty("prompty/basic.prompty")] +[Prompty("prompty/context.prompty")] +public class ClassWithMultipleAttributes { } + +[Prompty("prompty/basic.prompty", IsResource = true)] +[Prompty("prompty/context.prompty", Configuration = "FAKE_TYPE")] +public class ClassWithMultipleMixedAttributes { } + public class PromptyAttributeTests { public PromptyAttributeTests() @@ -100,4 +108,64 @@ public void LoadFromResourceWithConfigAndParams() Assert.Contains("Haglund", messages[0].Text); Assert.Contains("What is your name?", messages[1].Text); } + + /// + /// Test retrieving multiple Prompty attributes from a class + /// + [Fact] + public void LoadMultipleAttributes() + { + var attrs = Attribute.GetCustomAttributes( + typeof(ClassWithMultipleAttributes), + typeof(PromptyAttribute)); + + Assert.NotNull(attrs); + Assert.Equal(2, attrs.Length); + + var basicAttr = attrs[0] as PromptyAttribute; + var contextAttr = attrs[1] as PromptyAttribute; + + Assert.NotNull(basicAttr); + Assert.NotNull(contextAttr); + Assert.Equal("prompty/basic.prompty", basicAttr!.File); + Assert.Equal("prompty/context.prompty", contextAttr!.File); + + Assert.NotNull(basicAttr.Prompt); + Assert.NotNull(contextAttr.Prompt); + + Assert.NotNull(basicAttr.Messages); + Assert.NotNull(contextAttr.Messages); + } + + /// + /// Test retrieving multiple Prompty attributes with different configurations + /// + [Fact] + public void LoadMultipleMixedAttributes() + { + var attrs = Attribute.GetCustomAttributes( + typeof(ClassWithMultipleMixedAttributes), + typeof(PromptyAttribute)); + + Assert.NotNull(attrs); + Assert.Equal(2, attrs.Length); + + var basicAttr = attrs[0] as PromptyAttribute; + var contextAttr = attrs[1] as PromptyAttribute; + + Assert.NotNull(basicAttr); + Assert.NotNull(contextAttr); + + // First attribute with IsResource = true + Assert.Equal("prompty/basic.prompty", basicAttr!.File); + Assert.True(basicAttr.IsResource); + Assert.NotNull(basicAttr.Prompt); + Assert.NotNull(basicAttr.Messages); + + // Second attribute with specific configuration + Assert.Equal("prompty/context.prompty", contextAttr!.File); + Assert.Equal("FAKE_TYPE", contextAttr.Configuration); + Assert.NotNull(contextAttr.Prompt); + Assert.NotNull(contextAttr.Messages); + } } diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index dd151538..cd4bebee 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -17,6 +17,7 @@ namespace Prompty.Core; /// var messages = prompty.Messages; /// ... /// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class PromptyAttribute : Attribute { /// From 52940e027d0123842c38c1965c6d0b4cfc86007d Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Fri, 14 Mar 2025 12:47:49 -0700 Subject: [PATCH 06/11] move to lazy loading and make finding the resources possible in calling assemblies as well as current assembly --- .../PromptyAttributeTests.cs | 17 ++- .../Prompty.Core/PromptyAttribute.cs | 122 +++++++++++++++--- 2 files changed, 112 insertions(+), 27 deletions(-) diff --git a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs index f2b90ab5..72bcc7e6 100644 --- a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs +++ b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs @@ -36,12 +36,11 @@ public void LoadFromFile() var attr = (PromptyAttribute)Attribute.GetCustomAttribute( typeof(ClassWithAttribute), typeof(PromptyAttribute))!; - + Assert.NotNull(attr.Messages); Assert.NotNull(attr); Assert.Equal("prompty/basic.prompty", attr.File); Assert.False(attr.IsResource); Assert.NotNull(attr.Prompt); - Assert.NotNull(attr.Messages); } /// @@ -53,26 +52,32 @@ public void LoadFromResource() var attr = (PromptyAttribute)Attribute.GetCustomAttribute( typeof(ClassWithResourceAttribute), typeof(PromptyAttribute))!; - + Assert.NotNull(attr.Messages); Assert.NotNull(attr); Assert.Equal("prompty/basic.prompty", attr.File); Assert.True(attr.IsResource); Assert.NotNull(attr.Prompt); - Assert.NotNull(attr.Messages); } [Fact] public void ThrowsOnInvalidFile() { + Assert.Throws(() => - new PromptyAttribute("nonexistent.prompty", false)); + { + var fail = new PromptyAttribute("nonexistent.prompty", false); + var _ = fail.Prompt; + }); } [Fact] public void ThrowsOnInvalidResource() { Assert.Throws(() => - new PromptyAttribute("nonexistent.prompty", true)); + { + var fail = new PromptyAttribute("nonexistent.prompty", true); + var _ = fail.Prompt; + }); } /// diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index cd4bebee..1a4be608 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -42,32 +42,60 @@ public class PromptyAttribute : Attribute /// /// the loaded prompty /// - public Prompty Prompt { get; set; } + public Prompty? Prompt => GetPrompt(); /// /// The prepared messages /// - public ChatMessage[] Messages => (ChatMessage[])Prompt.Prepare(GetParams(), mergeSample: true); - + public ChatMessage[] Messages => GetMessages(); public PromptyAttribute(string File, bool IsResource = false, string Configuration = "default", string[] Params = null!) { this.File = File; this.IsResource = IsResource; this.Configuration = Configuration; this.Params = Params; + } - InvokerFactory.AutoDiscovery(); + /// + /// convert the params to a dictionary + /// + /// Dictionary + private Dictionary GetParams() + { + var dict = new Dictionary(); + if (Params != null) + { + for (int i = 0; i < Params.Length; i += 2) + { + if (i + 1 < Params.Length) + dict.Add(Params[i], Params[i + 1]); + } + } + return dict; + } + /// + /// Get the prompt from the file or resource + /// + /// Prompty + /// + private Prompty GetPrompt() + { + Prompty? prompt = null; if (IsResource == true) { - // get the stream from the resource name - var assembly = Assembly.GetExecutingAssembly(); - using var stream = assembly.GetManifestResourceStream(File); + // Try to get the resource from various assemblies + Stream? stream = FindResourceInAssemblies(File); + if (stream == null) { throw new FileNotFoundException($"Resource {File} not found"); } - this.Prompt = Prompty.Load(stream, Configuration); + + using (stream) + { + prompt = Prompty.Load(stream, Configuration); + } } else { @@ -76,25 +104,77 @@ public PromptyAttribute(string File, bool IsResource = false, string Configurati throw new FileNotFoundException($"File {File} not found"); } // load the file - this.Prompt = Prompty.Load(File, Configuration); + prompt = Prompty.Load(File, Configuration); } + return prompt; } - + /// - /// convert the params to a dictionary + /// Attempts to find a resource in multiple assemblies /// - /// Dictionary - public Dictionary GetParams() + /// The resource name to find + /// A Stream for the resource if found, null otherwise + private Stream? FindResourceInAssemblies(string resourceName) { - var dict = new Dictionary(); - if (Params != null) + // Normalize resource name to handle different path formats + var normalizedName = resourceName.Replace('\\', '.').Replace('/', '.'); + + // Helper function to check for resource in an assembly + Stream? TryGetResourceStream(Assembly assembly, string name) { - for (int i = 0; i < Params.Length; i += 2) - { - if (i + 1 < Params.Length) - dict.Add(Params[i], Params[i + 1]); - } + // Try direct match + var stream = assembly.GetManifestResourceStream(name); + if (stream != null) + return stream; + + // Try assembly qualified name + stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.{name}"); + if (stream != null) + return stream; + + // Try suffix match with all manifest resources + var resourceNames = assembly.GetManifestResourceNames(); + var matchingResource = resourceNames.FirstOrDefault(r => + r.EndsWith(normalizedName, StringComparison.OrdinalIgnoreCase) || + r.EndsWith(name, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(matchingResource)) + return assembly.GetManifestResourceStream(matchingResource); + + return null; } - return dict; + + // Try executing assembly (the assembly containing this code) + var executingAssembly = Assembly.GetExecutingAssembly(); + var stream = TryGetResourceStream(executingAssembly, resourceName); + if (stream != null) + return stream; + + // Try entry assembly (the main application assembly) + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly != null && entryAssembly != executingAssembly) + { + stream = TryGetResourceStream(entryAssembly, resourceName); + if (stream != null) + return stream; + } + + // Try all other loaded assemblies + return AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a != executingAssembly && a != entryAssembly) + .Select(a => TryGetResourceStream(a, resourceName)) + .FirstOrDefault(s => s != null); + } + /// + /// Get the messages from the prompt + /// + /// ChatMessage[] + /// + private ChatMessage[] GetMessages() + { + InvokerFactory.AutoDiscovery(); + if (Prompt == null) + throw new InvalidOperationException("Prompt is null"); + return (ChatMessage[])Prompt.Prepare(GetParams(), mergeSample: true); } } From a4ef16e6dbbf8c4e57e4404177a4fe7386e21c0e Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Fri, 14 Mar 2025 12:58:32 -0700 Subject: [PATCH 07/11] improve usage comment --- runtime/promptycs/Prompty.Core/PromptyAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index 1a4be608..0dd3e55f 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -8,7 +8,7 @@ namespace Prompty.Core; /// /// /// [Prompty("prompty/basic.prompty"] -/// [Prompty("prompty/embedded-resource-path.prompty", IsResource = true, Configuration = "FAKE_TYPE", Params = new string[] { "question", "answer" })] +/// [Prompty("prompty/embedded-resource-path.prompty", IsResource = true, Configuration = "default", Params = new string[] { "question", "answer" })] /// public class MyClass /// {...} /// in a class or method then use the attribute to load the prompty From 84e4b72784f0ecbbece0ed1bbdea3b4282dc1827 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Fri, 14 Mar 2025 13:08:26 -0700 Subject: [PATCH 08/11] formatting --- .../Prompty.Core/PromptyAttribute.cs | 107 +++++++++--------- 1 file changed, 54 insertions(+), 53 deletions(-) diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index 0dd3e55f..066757e6 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -56,59 +56,6 @@ public PromptyAttribute(string File, bool IsResource = false, string Configurati this.Params = Params; } - /// - /// convert the params to a dictionary - /// - /// Dictionary - private Dictionary GetParams() - { - var dict = new Dictionary(); - if (Params != null) - { - for (int i = 0; i < Params.Length; i += 2) - { - if (i + 1 < Params.Length) - dict.Add(Params[i], Params[i + 1]); - } - } - return dict; - } - - /// - /// Get the prompt from the file or resource - /// - /// Prompty - /// - private Prompty GetPrompt() - { - Prompty? prompt = null; - if (IsResource == true) - { - // Try to get the resource from various assemblies - Stream? stream = FindResourceInAssemblies(File); - - if (stream == null) - { - throw new FileNotFoundException($"Resource {File} not found"); - } - - using (stream) - { - prompt = Prompty.Load(stream, Configuration); - } - } - else - { - if (!System.IO.File.Exists(File)) - { - throw new FileNotFoundException($"File {File} not found"); - } - // load the file - prompt = Prompty.Load(File, Configuration); - } - return prompt; - } - /// /// Attempts to find a resource in multiple assemblies /// @@ -165,6 +112,60 @@ private Prompty GetPrompt() .Select(a => TryGetResourceStream(a, resourceName)) .FirstOrDefault(s => s != null); } + + /// + /// convert the params to a dictionary + /// + /// Dictionary + private Dictionary GetParams() + { + var dict = new Dictionary(); + if (Params != null) + { + for (int i = 0; i < Params.Length; i += 2) + { + if (i + 1 < Params.Length) + dict.Add(Params[i], Params[i + 1]); + } + } + return dict; + } + + /// + /// Get the prompt from the file or resource + /// + /// Prompty + /// + private Prompty GetPrompt() + { + Prompty? prompt = null; + if (IsResource == true) + { + // Try to get the resource from various assemblies + Stream? stream = FindResourceInAssemblies(File); + + if (stream == null) + { + throw new FileNotFoundException($"Resource {File} not found"); + } + + using (stream) + { + prompt = Prompty.Load(stream, Configuration); + } + } + else + { + if (!System.IO.File.Exists(File)) + { + throw new FileNotFoundException($"File {File} not found"); + } + // load the file + prompt = Prompty.Load(File, Configuration); + } + return prompt; + } + /// /// Get the messages from the prompt /// From 5ddefef1ae91231396fc0fecf68bcc999b234e26 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Fri, 14 Mar 2025 13:12:17 -0700 Subject: [PATCH 09/11] additional doc comments --- .../PromptyAttributeTests.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs index 72bcc7e6..4286bd9c 100644 --- a/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs +++ b/runtime/promptycs/Prompty.Core.Tests/PromptyAttributeTests.cs @@ -2,24 +2,42 @@ namespace Prompty.Core.Tests; - +/// +/// Test class with a single Prompty attribute +/// [Prompty("prompty/basic.prompty")] public class ClassWithAttribute { } +/// +/// Test class with a Prompty attribute that loads from an embedded resource +/// [Prompty("prompty/basic.prompty", IsResource = true)] public class ClassWithResourceAttribute { } +/// +/// Test class with a Prompty attribute that loads from an embedded resource +/// and has configuration and parameters +/// [Prompty("prompty/basic.prompty", IsResource = true, Configuration = "FAKE_TYPE", Params = new string[] { "firstName", "Caspar", "lastName", "Haglund", "question", "What is your name?" })] public class ClassWithResourceAttributeAndCofigAndParams { } +/// +/// Test class with multiple Prompty attributes +/// [Prompty("prompty/basic.prompty")] [Prompty("prompty/context.prompty")] public class ClassWithMultipleAttributes { } +/// +/// Test class with multiple Prompty attributes with mixed configurations +/// [Prompty("prompty/basic.prompty", IsResource = true)] [Prompty("prompty/context.prompty", Configuration = "FAKE_TYPE")] public class ClassWithMultipleMixedAttributes { } +/// +/// Prompty Attribute Tests +/// public class PromptyAttributeTests { public PromptyAttributeTests() @@ -59,6 +77,9 @@ public void LoadFromResource() Assert.NotNull(attr.Prompt); } + /// + /// Test that inalid file paths result in exception + /// [Fact] public void ThrowsOnInvalidFile() { @@ -70,6 +91,9 @@ public void ThrowsOnInvalidFile() }); } + /// + /// Test that invalid resource paths result in exception + /// [Fact] public void ThrowsOnInvalidResource() { From e3e95e5a11306bdec7354cf4ea265a01914615de Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Fri, 14 Mar 2025 13:15:23 -0700 Subject: [PATCH 10/11] formatting - primary constructor --- .../Prompty.Core/PromptyAttribute.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index 066757e6..73bdc4fb 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -18,26 +18,26 @@ namespace Prompty.Core; /// ... /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] -public class PromptyAttribute : Attribute +public class PromptyAttribute(string File, bool IsResource = false, string Configuration = "default", string[] Params = null!) : Attribute { /// /// The file name of the prompty file - public string File { get; set; } + public string File { get; set; } = File; /// /// Is the file a resource /// - public bool IsResource { get; set; } = false; - + public bool IsResource { get; set; } = IsResource; + /// /// The configuration id to use /// - public string? Configuration { get; set; } + public string? Configuration { get; set; } = Configuration; /// /// The parameters for input /// - public string[]? Params { get; set; } + public string[]? Params { get; set; } = Params; /// /// the loaded prompty @@ -48,13 +48,6 @@ public class PromptyAttribute : Attribute /// The prepared messages /// public ChatMessage[] Messages => GetMessages(); - public PromptyAttribute(string File, bool IsResource = false, string Configuration = "default", string[] Params = null!) - { - this.File = File; - this.IsResource = IsResource; - this.Configuration = Configuration; - this.Params = Params; - } /// /// Attempts to find a resource in multiple assemblies From 58927c96dfc472d7d48aad95a52e609aa7d0947c Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Wed, 19 Mar 2025 13:05:35 -0700 Subject: [PATCH 11/11] fix warnings --- runtime/promptycs/Prompty.Core/Prompty.cs | 6 ++++-- runtime/promptycs/Prompty.Core/PromptyAttribute.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/runtime/promptycs/Prompty.Core/Prompty.cs b/runtime/promptycs/Prompty.Core/Prompty.cs index 4c1e2b91..7170f375 100644 --- a/runtime/promptycs/Prompty.Core/Prompty.cs +++ b/runtime/promptycs/Prompty.Core/Prompty.cs @@ -192,7 +192,8 @@ public static Prompty Load(Stream stream, string configuration = "default") string text = reader.ReadToEnd(); var global_config = GlobalConfig.Load(System.IO.Path.GetDirectoryName(stream.ToString()) ?? string.Empty, configuration) ?? []; - global_config = Normalizer.Normalize(global_config, stream.ToString()); + var streamPath = stream.ToString() ?? string.Empty; + global_config = Normalizer.Normalize(global_config, streamPath); var frontmatter = LoadRaw(text, global_config, stream.ToString()); var prompty = Convert(frontmatter, stream.ToString()); @@ -210,7 +211,8 @@ public static async Task LoadAsync(Stream stream, string configuration string text = await reader.ReadToEndAsync(); var global_config = await GlobalConfig.LoadAsync(System.IO.Path.GetDirectoryName(stream.ToString()) ?? string.Empty, configuration) ?? []; - global_config = Normalizer.Normalize(global_config, stream.ToString()); + var streamPath = stream.ToString() ?? string.Empty; + global_config = Normalizer.Normalize(global_config, streamPath); var frontmatter = LoadRaw(text, global_config, stream.ToString()); var prompty = Convert(frontmatter, stream.ToString()); diff --git a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs index 73bdc4fb..13d0a712 100644 --- a/runtime/promptycs/Prompty.Core/PromptyAttribute.cs +++ b/runtime/promptycs/Prompty.Core/PromptyAttribute.cs @@ -144,7 +144,7 @@ private Prompty GetPrompt() using (stream) { - prompt = Prompty.Load(stream, Configuration); + prompt = Prompty.Load(stream, Configuration ?? "default"); } } else @@ -154,7 +154,7 @@ private Prompty GetPrompt() throw new FileNotFoundException($"File {File} not found"); } // load the file - prompt = Prompty.Load(File, Configuration); + prompt = Prompty.Load(File, Configuration ?? "default"); } return prompt; }