diff --git a/runtime/csharp/Prompty.Anthropic/AnthropicExecutor.cs b/runtime/csharp/Prompty.Anthropic/AnthropicExecutor.cs index e936c00c..cfc2313e 100644 --- a/runtime/csharp/Prompty.Anthropic/AnthropicExecutor.cs +++ b/runtime/csharp/Prompty.Anthropic/AnthropicExecutor.cs @@ -105,11 +105,18 @@ async IAsyncEnumerable StreamEvents([System.Runtime.CompilerServices.Enu if (stream) body["stream"] = true; + // Apply model options via generated wire mapping var opts = agent.Model?.Options; - if (opts?.Temperature is not null) body["temperature"] = opts.Temperature; - if (opts?.TopP is not null) body["top_p"] = opts.TopP; - if (opts?.TopK is not null) body["top_k"] = opts.TopK; - if (opts?.StopSequences is not null) body["stop_sequences"] = opts.StopSequences; + if (opts is not null) + { + var wireOpts = opts.ToWire("anthropic"); + // max_tokens is handled separately above with DefaultMaxTokens fallback + wireOpts.Remove("max_tokens"); + foreach (var (key, value) in wireOpts) + { + body[key] = value; + } + } var tools = ToolsToWire(agent); if (tools is not null) body["tools"] = tools; @@ -147,7 +154,7 @@ private static HttpRequestMessage CreateRequest(string endpoint, string apiKey, // Handle tool results if (msg.Role == Roles.Tool) { - var toolCallId = msg.Metadata.TryGetValue("tool_call_id", out var id) + var toolCallId = msg.Metadata is not null && msg.Metadata.TryGetValue("tool_call_id", out var id) ? id?.ToString() ?? "" : ""; return new Dictionary diff --git a/runtime/csharp/Prompty.Core.Tests/AgentLoopIntegrationTests.cs b/runtime/csharp/Prompty.Core.Tests/AgentLoopIntegrationTests.cs index e80f8a8f..73833dbc 100644 --- a/runtime/csharp/Prompty.Core.Tests/AgentLoopIntegrationTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/AgentLoopIntegrationTests.cs @@ -22,7 +22,7 @@ public Task RenderAsync(Prompty agent, string template, Dictionary file class PassthroughParser : IParser { - public Task> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) => Task.FromResult(new List { new() { Role = Roles.User, Parts = [new TextPart { Value = rendered }] } @@ -35,7 +35,7 @@ public Task> ParseAsync(Prompty agent, string rendered) /// file class MultiMessageParser : IParser { - public Task> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) { var filler = new string('z', 300); return Task.FromResult(new List @@ -282,7 +282,7 @@ public async Task InputGuardrail_RewritesMessages_ExecutorSeesRewritten() }; var guardrails = new Guardrails( - input: _ => new GuardrailResult(true, Rewrite: rewritten)); + input: _ => new GuardrailResult(true, rewrite: rewritten)); var result = await Pipeline.TurnAsync(agent, guardrails: guardrails); @@ -327,7 +327,7 @@ public async Task OutputGuardrail_RewritesResponse_ReturnsSanitised() var agent = AgentLoopHelper.CreateAgent(); var guardrails = new Guardrails( - output: _ => new GuardrailResult(true, Rewrite: "redacted answer")); + output: _ => new GuardrailResult(true, rewrite: "redacted answer")); var result = await Pipeline.TurnAsync(agent, guardrails: guardrails); @@ -367,7 +367,7 @@ public async Task ToolGuardrail_RewritesArgs_ToolReceivesRewrittenArgs() var rewrittenArgs = new Dictionary { ["city"] = "Seattle" }; var guardrails = new Guardrails( - tool: (name, args) => new GuardrailResult(true, Rewrite: rewrittenArgs)); + tool: (name, args) => new GuardrailResult(true, rewrite: rewrittenArgs)); var result = await Pipeline.TurnAsync(agent, tools: tools, guardrails: guardrails); diff --git a/runtime/csharp/Prompty.Core.Tests/Model/AnonymousConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/AnonymousConnectionConversionTests.cs index 0f6fa882..874f9da2 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/AnonymousConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/AnonymousConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ApiKeyConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ApiKeyConnectionConversionTests.cs index 3371ce0e..08653272 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ApiKeyConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ApiKeyConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ArrayPropertyConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ArrayPropertyConversionTests.cs index fa4a746e..f5c0bd95 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ArrayPropertyConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ArrayPropertyConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/AudioPartConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/AudioPartConversionTests.cs new file mode 100644 index 00000000..f30c32f9 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/AudioPartConversionTests.cs @@ -0,0 +1,122 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class AudioPartConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +source: "https://example.com/audio.wav" +mediaType: audio/wav + +"""; + + var instance = AudioPart.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("https://example.com/audio.wav", instance.Source); + Assert.Equal("audio/wav", instance.MediaType); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +"""; + + var instance = AudioPart.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("https://example.com/audio.wav", instance.Source); + Assert.Equal("audio/wav", instance.MediaType); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +"""; + + var original = AudioPart.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = AudioPart.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("https://example.com/audio.wav", reloaded.Source); + Assert.Equal("audio/wav", reloaded.MediaType); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +source: "https://example.com/audio.wav" +mediaType: audio/wav + +"""; + + var original = AudioPart.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = AudioPart.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("https://example.com/audio.wav", reloaded.Source); + Assert.Equal("audio/wav", reloaded.MediaType); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +"""; + + var instance = AudioPart.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +source: "https://example.com/audio.wav" +mediaType: audio/wav + +"""; + + var instance = AudioPart.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/BindingConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/BindingConversionTests.cs index 63c7756e..5ae77e47 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/BindingConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/BindingConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 @@ -120,6 +119,7 @@ public void ToYamlProducesValidYaml() var parsed = deserializer.Deserialize(yaml); Assert.NotNull(parsed); } + [Fact] public void LoadJsonFromString() { @@ -137,7 +137,7 @@ public void LoadYamlFromString() var data = "\"example\""; var instance = Binding.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("example", instance.Input); } - } diff --git a/runtime/csharp/Prompty.Core.Tests/Model/CompactionCompletePayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/CompactionCompletePayloadConversionTests.cs new file mode 100644 index 00000000..40f3a6c2 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/CompactionCompletePayloadConversionTests.cs @@ -0,0 +1,122 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class CompactionCompletePayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +removed: 5 +remaining: 3 + +"""; + + var instance = CompactionCompletePayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal(5, instance.Removed); + Assert.Equal(3, instance.Remaining); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "removed": 5, + "remaining": 3 +} +"""; + + var instance = CompactionCompletePayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal(5, instance.Removed); + Assert.Equal(3, instance.Remaining); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "removed": 5, + "remaining": 3 +} +"""; + + var original = CompactionCompletePayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = CompactionCompletePayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal(5, reloaded.Removed); + Assert.Equal(3, reloaded.Remaining); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +removed: 5 +remaining: 3 + +"""; + + var original = CompactionCompletePayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = CompactionCompletePayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal(5, reloaded.Removed); + Assert.Equal(3, reloaded.Remaining); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "removed": 5, + "remaining": 3 +} +"""; + + var instance = CompactionCompletePayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +removed: 5 +remaining: 3 + +"""; + + var instance = CompactionCompletePayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/CompactionConfigConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/CompactionConfigConversionTests.cs new file mode 100644 index 00000000..31c383c3 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/CompactionConfigConversionTests.cs @@ -0,0 +1,137 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class CompactionConfigConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +strategy: summarize +budget: 50000 +options: + preserveSystemMessages: true + +"""; + + var instance = CompactionConfig.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("summarize", instance.Strategy); + Assert.Equal(50000, instance.Budget); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +"""; + + var instance = CompactionConfig.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("summarize", instance.Strategy); + Assert.Equal(50000, instance.Budget); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +"""; + + var original = CompactionConfig.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = CompactionConfig.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("summarize", reloaded.Strategy); + Assert.Equal(50000, reloaded.Budget); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +strategy: summarize +budget: 50000 +options: + preserveSystemMessages: true + +"""; + + var original = CompactionConfig.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = CompactionConfig.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("summarize", reloaded.Strategy); + Assert.Equal(50000, reloaded.Budget); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +"""; + + var instance = CompactionConfig.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +strategy: summarize +budget: 50000 +options: + preserveSystemMessages: true + +"""; + + var instance = CompactionConfig.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/CompactionFailedPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/CompactionFailedPayloadConversionTests.cs new file mode 100644 index 00000000..23093a66 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/CompactionFailedPayloadConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class CompactionFailedPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +message: Summarization prompt exceeded context window + +"""; + + var instance = CompactionFailedPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Summarization prompt exceeded context window", instance.Message); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "message": "Summarization prompt exceeded context window" +} +"""; + + var instance = CompactionFailedPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Summarization prompt exceeded context window", instance.Message); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "message": "Summarization prompt exceeded context window" +} +"""; + + var original = CompactionFailedPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = CompactionFailedPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Summarization prompt exceeded context window", reloaded.Message); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +message: Summarization prompt exceeded context window + +"""; + + var original = CompactionFailedPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = CompactionFailedPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Summarization prompt exceeded context window", reloaded.Message); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "message": "Summarization prompt exceeded context window" +} +"""; + + var instance = CompactionFailedPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +message: Summarization prompt exceeded context window + +"""; + + var instance = CompactionFailedPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ConnectionConversionTests.cs index 6e258e5d..2c597798 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ContentPartConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ContentPartConversionTests.cs new file mode 100644 index 00000000..148e1f1d --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ContentPartConversionTests.cs @@ -0,0 +1,10 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ContentPartConversionTests +{ +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/CustomToolConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/CustomToolConversionTests.cs index 489dbf69..9586ab5e 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/CustomToolConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/CustomToolConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/DoneEventPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/DoneEventPayloadConversionTests.cs new file mode 100644 index 00000000..1211f745 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/DoneEventPayloadConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class DoneEventPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +response: The weather in Paris is 72°F and sunny. + +"""; + + var instance = DoneEventPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("The weather in Paris is 72°F and sunny.", instance.Response); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "response": "The weather in Paris is 72°F and sunny." +} +"""; + + var instance = DoneEventPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("The weather in Paris is 72°F and sunny.", instance.Response); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "response": "The weather in Paris is 72°F and sunny." +} +"""; + + var original = DoneEventPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = DoneEventPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("The weather in Paris is 72°F and sunny.", reloaded.Response); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +response: The weather in Paris is 72°F and sunny. + +"""; + + var original = DoneEventPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = DoneEventPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("The weather in Paris is 72°F and sunny.", reloaded.Response); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "response": "The weather in Paris is 72°F and sunny." +} +"""; + + var instance = DoneEventPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +response: The weather in Paris is 72°F and sunny. + +"""; + + var instance = DoneEventPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ErrorChunkConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ErrorChunkConversionTests.cs new file mode 100644 index 00000000..16153964 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ErrorChunkConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ErrorChunkConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +message: Rate limit exceeded + +"""; + + var instance = ErrorChunk.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Rate limit exceeded", instance.Message); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "message": "Rate limit exceeded" +} +"""; + + var instance = ErrorChunk.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Rate limit exceeded", instance.Message); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "message": "Rate limit exceeded" +} +"""; + + var original = ErrorChunk.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ErrorChunk.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Rate limit exceeded", reloaded.Message); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +message: Rate limit exceeded + +"""; + + var original = ErrorChunk.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ErrorChunk.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Rate limit exceeded", reloaded.Message); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "message": "Rate limit exceeded" +} +"""; + + var instance = ErrorChunk.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +message: Rate limit exceeded + +"""; + + var instance = ErrorChunk.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ErrorEventPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ErrorEventPayloadConversionTests.cs new file mode 100644 index 00000000..d3cc78e2 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ErrorEventPayloadConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ErrorEventPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +message: Rate limit exceeded + +"""; + + var instance = ErrorEventPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Rate limit exceeded", instance.Message); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "message": "Rate limit exceeded" +} +"""; + + var instance = ErrorEventPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Rate limit exceeded", instance.Message); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "message": "Rate limit exceeded" +} +"""; + + var original = ErrorEventPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ErrorEventPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Rate limit exceeded", reloaded.Message); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +message: Rate limit exceeded + +"""; + + var original = ErrorEventPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ErrorEventPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Rate limit exceeded", reloaded.Message); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "message": "Rate limit exceeded" +} +"""; + + var instance = ErrorEventPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +message: Rate limit exceeded + +"""; + + var instance = ErrorEventPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/FilePartConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/FilePartConversionTests.cs new file mode 100644 index 00000000..9d5e98dd --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/FilePartConversionTests.cs @@ -0,0 +1,122 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class FilePartConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +source: "https://example.com/document.pdf" +mediaType: application/pdf + +"""; + + var instance = FilePart.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("https://example.com/document.pdf", instance.Source); + Assert.Equal("application/pdf", instance.MediaType); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +"""; + + var instance = FilePart.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("https://example.com/document.pdf", instance.Source); + Assert.Equal("application/pdf", instance.MediaType); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +"""; + + var original = FilePart.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = FilePart.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("https://example.com/document.pdf", reloaded.Source); + Assert.Equal("application/pdf", reloaded.MediaType); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +source: "https://example.com/document.pdf" +mediaType: application/pdf + +"""; + + var original = FilePart.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = FilePart.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("https://example.com/document.pdf", reloaded.Source); + Assert.Equal("application/pdf", reloaded.MediaType); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +"""; + + var instance = FilePart.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +source: "https://example.com/document.pdf" +mediaType: application/pdf + +"""; + + var instance = FilePart.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/FormatConfigConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/FormatConfigConversionTests.cs index a4ad1ca3..b93f51bf 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/FormatConfigConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/FormatConfigConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 @@ -135,6 +134,7 @@ public void ToYamlProducesValidYaml() var parsed = deserializer.Deserialize(yaml); Assert.NotNull(parsed); } + [Fact] public void LoadJsonFromString() { @@ -152,7 +152,7 @@ public void LoadYamlFromString() var data = "\"example\""; var instance = FormatConfig.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("example", instance.Kind); } - } diff --git a/runtime/csharp/Prompty.Core.Tests/Model/FoundryConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/FoundryConnectionConversionTests.cs index 6d08b1a8..e8b36bf4 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/FoundryConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/FoundryConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/FunctionToolConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/FunctionToolConversionTests.cs index 08ce7851..079b3c9c 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/FunctionToolConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/FunctionToolConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/GuardrailResultConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/GuardrailResultConversionTests.cs new file mode 100644 index 00000000..1f5aa6e3 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/GuardrailResultConversionTests.cs @@ -0,0 +1,146 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class GuardrailResultConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +allowed: true +reason: Content is safe + +"""; + + var instance = GuardrailResult.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.True(instance.Allowed); + Assert.Equal("Content is safe", instance.Reason); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "allowed": true, + "reason": "Content is safe" +} +"""; + + var instance = GuardrailResult.FromJson(jsonData); + Assert.NotNull(instance); + Assert.True(instance.Allowed); + Assert.Equal("Content is safe", instance.Reason); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "allowed": true, + "reason": "Content is safe" +} +"""; + + var original = GuardrailResult.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = GuardrailResult.FromJson(json); + Assert.NotNull(reloaded); + Assert.True(reloaded.Allowed); + Assert.Equal("Content is safe", reloaded.Reason); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +allowed: true +reason: Content is safe + +"""; + + var original = GuardrailResult.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = GuardrailResult.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.True(reloaded.Allowed); + Assert.Equal("Content is safe", reloaded.Reason); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "allowed": true, + "reason": "Content is safe" +} +"""; + + var instance = GuardrailResult.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +allowed: true +reason: Content is safe + +"""; + + var instance = GuardrailResult.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } + + [Fact] + public void FactoryCreateRewrite() + { + var instance = GuardrailResult.CreateRewrite("test"); + Assert.NotNull(instance); + Assert.True(instance.Allowed); + } + + [Fact] + public void FactoryDeny() + { + var instance = GuardrailResult.Deny("test"); + Assert.NotNull(instance); + Assert.False(instance.Allowed); + } + + [Fact] + public void FactoryAllow() + { + var instance = GuardrailResult.Allow(); + Assert.NotNull(instance); + Assert.True(instance.Allowed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ImagePartConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ImagePartConversionTests.cs new file mode 100644 index 00000000..cbca1f7a --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ImagePartConversionTests.cs @@ -0,0 +1,132 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ImagePartConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +source: "https://example.com/image.png" +detail: auto +mediaType: image/png + +"""; + + var instance = ImagePart.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("https://example.com/image.png", instance.Source); + Assert.Equal("auto", instance.Detail); + Assert.Equal("image/png", instance.MediaType); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +"""; + + var instance = ImagePart.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("https://example.com/image.png", instance.Source); + Assert.Equal("auto", instance.Detail); + Assert.Equal("image/png", instance.MediaType); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +"""; + + var original = ImagePart.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ImagePart.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("https://example.com/image.png", reloaded.Source); + Assert.Equal("auto", reloaded.Detail); + Assert.Equal("image/png", reloaded.MediaType); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +source: "https://example.com/image.png" +detail: auto +mediaType: image/png + +"""; + + var original = ImagePart.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ImagePart.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("https://example.com/image.png", reloaded.Source); + Assert.Equal("auto", reloaded.Detail); + Assert.Equal("image/png", reloaded.MediaType); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +"""; + + var instance = ImagePart.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +source: "https://example.com/image.png" +detail: auto +mediaType: image/png + +"""; + + var instance = ImagePart.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/McpApprovalModeConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/McpApprovalModeConversionTests.cs index 18b1d180..a1c356c5 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/McpApprovalModeConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/McpApprovalModeConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 @@ -140,6 +139,7 @@ public void ToYamlProducesValidYaml() var parsed = deserializer.Deserialize(yaml); Assert.NotNull(parsed); } + [Fact] public void LoadJsonFromString() { @@ -157,7 +157,7 @@ public void LoadYamlFromString() var data = "\"never\""; var instance = McpApprovalMode.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("never", instance.Kind); } - } diff --git a/runtime/csharp/Prompty.Core.Tests/Model/McpToolConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/McpToolConversionTests.cs index aa7d1f65..5fec1d09 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/McpToolConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/McpToolConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/MessageConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/MessageConversionTests.cs new file mode 100644 index 00000000..6a8d39d1 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/MessageConversionTests.cs @@ -0,0 +1,178 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class MessageConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +role: user +parts: + - kind: text + value: Hello! +metadata: + source: user-input + +"""; + + var instance = Message.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("user", instance.Role); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +"""; + + var instance = Message.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("user", instance.Role); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +"""; + + var original = Message.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = Message.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("user", reloaded.Role); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +role: user +parts: + - kind: text + value: Hello! +metadata: + source: user-input + +"""; + + var original = Message.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = Message.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("user", reloaded.Role); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +"""; + + var instance = Message.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +role: user +parts: + - kind: text + value: Hello! +metadata: + source: user-input + +"""; + + var instance = Message.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } + + [Fact] + public void FactoryAssistant() + { + var instance = Message.Assistant("test"); + Assert.NotNull(instance); + Assert.Equal("assistant", instance.Role); + } + + [Fact] + public void FactorySystem() + { + var instance = Message.System("test"); + Assert.NotNull(instance); + Assert.Equal("system", instance.Role); + } + + [Fact] + public void FactoryUser() + { + var instance = Message.User("test"); + Assert.NotNull(instance); + Assert.Equal("user", instance.Role); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/MessagesUpdatedPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/MessagesUpdatedPayloadConversionTests.cs new file mode 100644 index 00000000..8a059d2c --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/MessagesUpdatedPayloadConversionTests.cs @@ -0,0 +1,10 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class MessagesUpdatedPayloadConversionTests +{ +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ModelConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ModelConversionTests.cs index cf7eca2e..8caf5761 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ModelConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ModelConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 @@ -184,6 +183,7 @@ public void ToYamlProducesValidYaml() var parsed = deserializer.Deserialize(yaml); Assert.NotNull(parsed); } + [Fact] public void LoadJsonFromString() { @@ -201,7 +201,7 @@ public void LoadYamlFromString() var data = "\"example\""; var instance = Model.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("example", instance.Id); } - } diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ModelInfoConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ModelInfoConversionTests.cs new file mode 100644 index 00000000..61f89034 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ModelInfoConversionTests.cs @@ -0,0 +1,193 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ModelInfoConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +id: gpt-4o +displayName: GPT-4o +ownedBy: openai +contextWindow: 128000 +inputModalities: + - text + - image +outputModalities: + - text +additionalProperties: + supportsStreaming: true + +"""; + + var instance = ModelInfo.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("gpt-4o", instance.Id); + Assert.Equal("GPT-4o", instance.DisplayName); + Assert.Equal("openai", instance.OwnedBy); + Assert.Equal(128000, instance.ContextWindow); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +"""; + + var instance = ModelInfo.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("gpt-4o", instance.Id); + Assert.Equal("GPT-4o", instance.DisplayName); + Assert.Equal("openai", instance.OwnedBy); + Assert.Equal(128000, instance.ContextWindow); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +"""; + + var original = ModelInfo.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ModelInfo.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("gpt-4o", reloaded.Id); + Assert.Equal("GPT-4o", reloaded.DisplayName); + Assert.Equal("openai", reloaded.OwnedBy); + Assert.Equal(128000, reloaded.ContextWindow); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +id: gpt-4o +displayName: GPT-4o +ownedBy: openai +contextWindow: 128000 +inputModalities: + - text + - image +outputModalities: + - text +additionalProperties: + supportsStreaming: true + +"""; + + var original = ModelInfo.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ModelInfo.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("gpt-4o", reloaded.Id); + Assert.Equal("GPT-4o", reloaded.DisplayName); + Assert.Equal("openai", reloaded.OwnedBy); + Assert.Equal(128000, reloaded.ContextWindow); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +"""; + + var instance = ModelInfo.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +id: gpt-4o +displayName: GPT-4o +ownedBy: openai +contextWindow: 128000 +inputModalities: + - text + - image +outputModalities: + - text +additionalProperties: + supportsStreaming: true + +"""; + + var instance = ModelInfo.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ModelOptionsConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ModelOptionsConversionTests.cs index f3a04f5c..dc497430 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ModelOptionsConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ModelOptionsConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/OAuthConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/OAuthConnectionConversionTests.cs index 0481b1dc..de6b3f4f 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/OAuthConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/OAuthConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ObjectPropertyConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ObjectPropertyConversionTests.cs index c86da3fb..c09c7338 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ObjectPropertyConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ObjectPropertyConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/OpenApiToolConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/OpenApiToolConversionTests.cs index 442cd4dd..e1678895 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/OpenApiToolConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/OpenApiToolConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ParserConfigConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ParserConfigConversionTests.cs index 114e4b93..1f2edafe 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ParserConfigConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ParserConfigConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 @@ -125,6 +124,7 @@ public void ToYamlProducesValidYaml() var parsed = deserializer.Deserialize(yaml); Assert.NotNull(parsed); } + [Fact] public void LoadJsonFromString() { @@ -142,7 +142,7 @@ public void LoadYamlFromString() var data = "\"example\""; var instance = ParserConfig.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("example", instance.Kind); } - } diff --git a/runtime/csharp/Prompty.Core.Tests/Model/PromptyConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/PromptyConversionTests.cs index 6b302841..9b6b2548 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/PromptyConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/PromptyConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/PromptyToolConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/PromptyToolConversionTests.cs index e6eca0ca..15745977 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/PromptyToolConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/PromptyToolConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/PropertyConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/PropertyConversionTests.cs index 4abf8d97..0119ddf9 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/PropertyConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/PropertyConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 @@ -187,6 +186,7 @@ public void ToYamlProducesValidYaml() var parsed = deserializer.Deserialize(yaml); Assert.NotNull(parsed); } + [Fact] public void LoadJsonFromBoolean() { @@ -207,11 +207,13 @@ public void LoadYamlFromBoolean() var data = "false"; var instance = Property.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("boolean", instance.Kind); Assert.NotNull(instance.Example); Assert.IsType(instance.Example); Assert.False((bool)instance.Example); } + [Fact] public void LoadJsonFromFloat32() { @@ -232,11 +234,13 @@ public void LoadYamlFromFloat32() var data = "3.14"; var instance = Property.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("float", instance.Kind); Assert.NotNull(instance.Example); Assert.True(instance.Example is float || instance.Example is double || instance.Example is int || instance.Example is long); Assert.Equal(3.14, Convert.ToDouble(instance.Example), 5); } + [Fact] public void LoadJsonFromInteger() { @@ -255,9 +259,11 @@ public void LoadYamlFromInteger() var data = "4"; var instance = Property.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("integer", instance.Kind); Assert.Equal(4, instance.Example); } + [Fact] public void LoadJsonFromString() { @@ -276,8 +282,8 @@ public void LoadYamlFromString() var data = "\"example\""; var instance = Property.FromYaml(data); Assert.NotNull(instance); + Assert.Equal("string", instance.Kind); Assert.Equal("example", instance.Example); } - } diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ReferenceConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ReferenceConnectionConversionTests.cs index 18557abd..09c63f41 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ReferenceConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ReferenceConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/RemoteConnectionConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/RemoteConnectionConversionTests.cs index b1a80558..8468e835 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/RemoteConnectionConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/RemoteConnectionConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/StatusEventPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/StatusEventPayloadConversionTests.cs new file mode 100644 index 00000000..1b5ac43e --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/StatusEventPayloadConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class StatusEventPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +message: Starting iteration 3 + +"""; + + var instance = StatusEventPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Starting iteration 3", instance.Message); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "message": "Starting iteration 3" +} +"""; + + var instance = StatusEventPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Starting iteration 3", instance.Message); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "message": "Starting iteration 3" +} +"""; + + var original = StatusEventPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = StatusEventPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Starting iteration 3", reloaded.Message); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +message: Starting iteration 3 + +"""; + + var original = StatusEventPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = StatusEventPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Starting iteration 3", reloaded.Message); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "message": "Starting iteration 3" +} +"""; + + var instance = StatusEventPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +message: Starting iteration 3 + +"""; + + var instance = StatusEventPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/StreamChunkConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/StreamChunkConversionTests.cs new file mode 100644 index 00000000..16733bb8 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/StreamChunkConversionTests.cs @@ -0,0 +1,10 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class StreamChunkConversionTests +{ +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/TemplateConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/TemplateConversionTests.cs index 0cb974eb..695bbc8c 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/TemplateConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/TemplateConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/TextChunkConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/TextChunkConversionTests.cs new file mode 100644 index 00000000..97c129ad --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/TextChunkConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class TextChunkConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +value: Hello + +"""; + + var instance = TextChunk.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Hello", instance.Value); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "value": "Hello" +} +"""; + + var instance = TextChunk.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Hello", instance.Value); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "value": "Hello" +} +"""; + + var original = TextChunk.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = TextChunk.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Hello", reloaded.Value); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +value: Hello + +"""; + + var original = TextChunk.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = TextChunk.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Hello", reloaded.Value); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "value": "Hello" +} +"""; + + var instance = TextChunk.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +value: Hello + +"""; + + var instance = TextChunk.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/TextPartConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/TextPartConversionTests.cs new file mode 100644 index 00000000..43932a86 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/TextPartConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class TextPartConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +value: Hello, world! + +"""; + + var instance = TextPart.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Hello, world!", instance.Value); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "value": "Hello, world!" +} +"""; + + var instance = TextPart.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Hello, world!", instance.Value); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "value": "Hello, world!" +} +"""; + + var original = TextPart.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = TextPart.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Hello, world!", reloaded.Value); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +value: Hello, world! + +"""; + + var original = TextPart.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = TextPart.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Hello, world!", reloaded.Value); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "value": "Hello, world!" +} +"""; + + var instance = TextPart.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +value: Hello, world! + +"""; + + var instance = TextPart.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ThinkingChunkConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ThinkingChunkConversionTests.cs new file mode 100644 index 00000000..3d2495a8 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ThinkingChunkConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ThinkingChunkConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +value: Let me consider... + +"""; + + var instance = ThinkingChunk.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Let me consider...", instance.Value); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "value": "Let me consider..." +} +"""; + + var instance = ThinkingChunk.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Let me consider...", instance.Value); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "value": "Let me consider..." +} +"""; + + var original = ThinkingChunk.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ThinkingChunk.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Let me consider...", reloaded.Value); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +value: Let me consider... + +"""; + + var original = ThinkingChunk.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ThinkingChunk.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Let me consider...", reloaded.Value); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "value": "Let me consider..." +} +"""; + + var instance = ThinkingChunk.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +value: Let me consider... + +"""; + + var instance = ThinkingChunk.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ThinkingEventPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ThinkingEventPayloadConversionTests.cs new file mode 100644 index 00000000..3731ba7d --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ThinkingEventPayloadConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ThinkingEventPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +token: Let me consider... + +"""; + + var instance = ThinkingEventPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Let me consider...", instance.Token); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "token": "Let me consider..." +} +"""; + + var instance = ThinkingEventPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Let me consider...", instance.Token); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "token": "Let me consider..." +} +"""; + + var original = ThinkingEventPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ThinkingEventPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Let me consider...", reloaded.Token); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +token: Let me consider... + +"""; + + var original = ThinkingEventPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ThinkingEventPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Let me consider...", reloaded.Token); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "token": "Let me consider..." +} +"""; + + var instance = ThinkingEventPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +token: Let me consider... + +"""; + + var instance = ThinkingEventPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ThreadMarkerConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ThreadMarkerConversionTests.cs new file mode 100644 index 00000000..04d59ff4 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ThreadMarkerConversionTests.cs @@ -0,0 +1,122 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ThreadMarkerConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +name: thread +kind: thread + +"""; + + var instance = ThreadMarker.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("thread", instance.Name); + Assert.Equal("thread", instance.Kind); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "name": "thread", + "kind": "thread" +} +"""; + + var instance = ThreadMarker.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("thread", instance.Name); + Assert.Equal("thread", instance.Kind); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "name": "thread", + "kind": "thread" +} +"""; + + var original = ThreadMarker.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ThreadMarker.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("thread", reloaded.Name); + Assert.Equal("thread", reloaded.Kind); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +name: thread +kind: thread + +"""; + + var original = ThreadMarker.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ThreadMarker.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("thread", reloaded.Name); + Assert.Equal("thread", reloaded.Kind); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "name": "thread", + "kind": "thread" +} +"""; + + var instance = ThreadMarker.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +name: thread +kind: thread + +"""; + + var instance = ThreadMarker.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/TokenEventPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/TokenEventPayloadConversionTests.cs new file mode 100644 index 00000000..cff968d0 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/TokenEventPayloadConversionTests.cs @@ -0,0 +1,112 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class TokenEventPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +token: Hello + +"""; + + var instance = TokenEventPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("Hello", instance.Token); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "token": "Hello" +} +"""; + + var instance = TokenEventPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("Hello", instance.Token); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "token": "Hello" +} +"""; + + var original = TokenEventPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = TokenEventPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("Hello", reloaded.Token); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +token: Hello + +"""; + + var original = TokenEventPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = TokenEventPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("Hello", reloaded.Token); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "token": "Hello" +} +"""; + + var instance = TokenEventPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +token: Hello + +"""; + + var instance = TokenEventPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/TokenUsageConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/TokenUsageConversionTests.cs new file mode 100644 index 00000000..13605019 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/TokenUsageConversionTests.cs @@ -0,0 +1,132 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class TokenUsageConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +promptTokens: 150 +completionTokens: 42 +totalTokens: 192 + +"""; + + var instance = TokenUsage.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal(150, instance.PromptTokens); + Assert.Equal(42, instance.CompletionTokens); + Assert.Equal(192, instance.TotalTokens); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +"""; + + var instance = TokenUsage.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal(150, instance.PromptTokens); + Assert.Equal(42, instance.CompletionTokens); + Assert.Equal(192, instance.TotalTokens); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +"""; + + var original = TokenUsage.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = TokenUsage.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal(150, reloaded.PromptTokens); + Assert.Equal(42, reloaded.CompletionTokens); + Assert.Equal(192, reloaded.TotalTokens); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +promptTokens: 150 +completionTokens: 42 +totalTokens: 192 + +"""; + + var original = TokenUsage.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = TokenUsage.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal(150, reloaded.PromptTokens); + Assert.Equal(42, reloaded.CompletionTokens); + Assert.Equal(192, reloaded.TotalTokens); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +"""; + + var instance = TokenUsage.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +promptTokens: 150 +completionTokens: 42 +totalTokens: 192 + +"""; + + var instance = TokenUsage.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolCallConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolCallConversionTests.cs new file mode 100644 index 00000000..d21b2191 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolCallConversionTests.cs @@ -0,0 +1,132 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolCallConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +id: call_abc123 +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +"""; + + var instance = ToolCall.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("call_abc123", instance.Id); + Assert.Equal("get_weather", instance.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), instance.Arguments); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +"""; + + var instance = ToolCall.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("call_abc123", instance.Id); + Assert.Equal("get_weather", instance.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), instance.Arguments); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +"""; + + var original = ToolCall.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolCall.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("call_abc123", reloaded.Id); + Assert.Equal("get_weather", reloaded.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), reloaded.Arguments); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +id: call_abc123 +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +"""; + + var original = ToolCall.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolCall.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("call_abc123", reloaded.Id); + Assert.Equal("get_weather", reloaded.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), reloaded.Arguments); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +"""; + + var instance = ToolCall.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +id: call_abc123 +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +"""; + + var instance = ToolCall.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolCallStartPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolCallStartPayloadConversionTests.cs new file mode 100644 index 00000000..7c907643 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolCallStartPayloadConversionTests.cs @@ -0,0 +1,122 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolCallStartPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +"""; + + var instance = ToolCallStartPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("get_weather", instance.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), instance.Arguments); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +"""; + + var instance = ToolCallStartPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("get_weather", instance.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), instance.Arguments); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +"""; + + var original = ToolCallStartPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolCallStartPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("get_weather", reloaded.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), reloaded.Arguments); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +"""; + + var original = ToolCallStartPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolCallStartPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("get_weather", reloaded.Name); + Assert.Equal(@"{""city"": ""Paris""}".Replace("\r\n", "\n"), reloaded.Arguments); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +"""; + + var instance = ToolCallStartPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +"""; + + var instance = ToolCallStartPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolChunkConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolChunkConversionTests.cs new file mode 100644 index 00000000..ef3b3c8c --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolChunkConversionTests.cs @@ -0,0 +1,129 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolChunkConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +toolCall: + id: call_abc123 + name: get_weather + arguments: "{\"city\": \"Paris\"}" + +"""; + + var instance = ToolChunk.FromYaml(yamlData); + + Assert.NotNull(instance); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +"""; + + var instance = ToolChunk.FromJson(jsonData); + Assert.NotNull(instance); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +"""; + + var original = ToolChunk.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolChunk.FromJson(json); + Assert.NotNull(reloaded); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +toolCall: + id: call_abc123 + name: get_weather + arguments: "{\"city\": \"Paris\"}" + +"""; + + var original = ToolChunk.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolChunk.FromYaml(yaml); + Assert.NotNull(reloaded); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +"""; + + var instance = ToolChunk.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +toolCall: + id: call_abc123 + name: get_weather + arguments: "{\"city\": \"Paris\"}" + +"""; + + var instance = ToolChunk.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolContextConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolContextConversionTests.cs new file mode 100644 index 00000000..beee477a --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolContextConversionTests.cs @@ -0,0 +1,117 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolContextConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +metadata: + userId: user-123 + +"""; + + var instance = ToolContext.FromYaml(yamlData); + + Assert.NotNull(instance); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "metadata": { + "userId": "user-123" + } +} +"""; + + var instance = ToolContext.FromJson(jsonData); + Assert.NotNull(instance); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "metadata": { + "userId": "user-123" + } +} +"""; + + var original = ToolContext.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolContext.FromJson(json); + Assert.NotNull(reloaded); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +metadata: + userId: user-123 + +"""; + + var original = ToolContext.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolContext.FromYaml(yaml); + Assert.NotNull(reloaded); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "metadata": { + "userId": "user-123" + } +} +"""; + + var instance = ToolContext.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +metadata: + userId: user-123 + +"""; + + var instance = ToolContext.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolConversionTests.cs index 69117ff1..3ffb95e5 100644 --- a/runtime/csharp/Prompty.Core.Tests/Model/ToolConversionTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolConversionTests.cs @@ -1,4 +1,3 @@ - using Xunit; #pragma warning disable IDE0130 diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolDispatchResultConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolDispatchResultConversionTests.cs new file mode 100644 index 00000000..f442e5d8 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolDispatchResultConversionTests.cs @@ -0,0 +1,158 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolDispatchResultConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +toolCallId: call_abc123 +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +"""; + + var instance = ToolDispatchResult.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("call_abc123", instance.ToolCallId); + Assert.Equal("get_weather", instance.Name); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +"""; + + var instance = ToolDispatchResult.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("call_abc123", instance.ToolCallId); + Assert.Equal("get_weather", instance.Name); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +"""; + + var original = ToolDispatchResult.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolDispatchResult.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("call_abc123", reloaded.ToolCallId); + Assert.Equal("get_weather", reloaded.Name); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +toolCallId: call_abc123 +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +"""; + + var original = ToolDispatchResult.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolDispatchResult.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("call_abc123", reloaded.ToolCallId); + Assert.Equal("get_weather", reloaded.Name); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +"""; + + var instance = ToolDispatchResult.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +toolCallId: call_abc123 +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +"""; + + var instance = ToolDispatchResult.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolResultConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolResultConversionTests.cs new file mode 100644 index 00000000..ab7b0219 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolResultConversionTests.cs @@ -0,0 +1,136 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolResultConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +parts: + - kind: text + value: 72°F and sunny + +"""; + + var instance = ToolResult.FromYaml(yamlData); + + Assert.NotNull(instance); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +"""; + + var instance = ToolResult.FromJson(jsonData); + Assert.NotNull(instance); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +"""; + + var original = ToolResult.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolResult.FromJson(json); + Assert.NotNull(reloaded); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +parts: + - kind: text + value: 72°F and sunny + +"""; + + var original = ToolResult.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolResult.FromYaml(yaml); + Assert.NotNull(reloaded); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +"""; + + var instance = ToolResult.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +parts: + - kind: text + value: 72°F and sunny + +"""; + + var instance = ToolResult.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } + + [Fact] + public void FactoryText() + { + var instance = ToolResult.CreateText("test"); + Assert.NotNull(instance); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/ToolResultPayloadConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/ToolResultPayloadConversionTests.cs new file mode 100644 index 00000000..adf10d5e --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/ToolResultPayloadConversionTests.cs @@ -0,0 +1,148 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class ToolResultPayloadConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +"""; + + var instance = ToolResultPayload.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("get_weather", instance.Name); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +"""; + + var instance = ToolResultPayload.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("get_weather", instance.Name); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +"""; + + var original = ToolResultPayload.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolResultPayload.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("get_weather", reloaded.Name); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +"""; + + var original = ToolResultPayload.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolResultPayload.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("get_weather", reloaded.Name); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +"""; + + var instance = ToolResultPayload.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +"""; + + var instance = ToolResultPayload.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/Model/TurnOptionsConversionTests.cs b/runtime/csharp/Prompty.Core.Tests/Model/TurnOptionsConversionTests.cs new file mode 100644 index 00000000..901a5bc2 --- /dev/null +++ b/runtime/csharp/Prompty.Core.Tests/Model/TurnOptionsConversionTests.cs @@ -0,0 +1,177 @@ +using Xunit; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + + +public class TurnOptionsConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +maxIterations: 10 +maxLlmRetries: 3 +contextBudget: 100000 +parallelToolCalls: true +raw: false +turn: 1 +compaction: + strategy: summarize + +"""; + + var instance = TurnOptions.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal(10, instance.MaxIterations); + Assert.Equal(3, instance.MaxLlmRetries); + Assert.Equal(100000, instance.ContextBudget); + Assert.True(instance.ParallelToolCalls); + Assert.False(instance.Raw); + Assert.Equal(1, instance.Turn); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +"""; + + var instance = TurnOptions.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal(10, instance.MaxIterations); + Assert.Equal(3, instance.MaxLlmRetries); + Assert.Equal(100000, instance.ContextBudget); + Assert.True(instance.ParallelToolCalls); + Assert.False(instance.Raw); + Assert.Equal(1, instance.Turn); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +"""; + + var original = TurnOptions.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = TurnOptions.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal(10, reloaded.MaxIterations); + Assert.Equal(3, reloaded.MaxLlmRetries); + Assert.Equal(100000, reloaded.ContextBudget); + Assert.True(reloaded.ParallelToolCalls); + Assert.False(reloaded.Raw); + Assert.Equal(1, reloaded.Turn); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +maxIterations: 10 +maxLlmRetries: 3 +contextBudget: 100000 +parallelToolCalls: true +raw: false +turn: 1 +compaction: + strategy: summarize + +"""; + + var original = TurnOptions.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = TurnOptions.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal(10, reloaded.MaxIterations); + Assert.Equal(3, reloaded.MaxLlmRetries); + Assert.Equal(100000, reloaded.ContextBudget); + Assert.True(reloaded.ParallelToolCalls); + Assert.False(reloaded.Raw); + Assert.Equal(1, reloaded.Turn); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +"""; + + var instance = TurnOptions.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +maxIterations: 10 +maxLlmRetries: 3 +contextBudget: 100000 +parallelToolCalls: true +raw: false +turn: 1 +compaction: + strategy: summarize + +"""; + + var instance = TurnOptions.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/Prompty.Core.Tests/ParserTests.cs b/runtime/csharp/Prompty.Core.Tests/ParserTests.cs index 13cdc0bb..3df12973 100644 --- a/runtime/csharp/Prompty.Core.Tests/ParserTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/ParserTests.cs @@ -15,7 +15,7 @@ public class PromptyChatParserTests [Fact] public async Task Parse_SingleSystemRole() { - var messages = await _parser.ParseAsync(CreateAgent(), "system:\nYou are helpful."); + var messages = await _parser.ParseAsync(CreateAgent(), "system:\nYou are helpful.", null); Assert.Single(messages); Assert.Equal(Roles.System, messages[0].Role); Assert.Equal("You are helpful.", messages[0].Text); @@ -25,7 +25,7 @@ public async Task Parse_SingleSystemRole() public async Task Parse_MultipleRoles() { var text = "system:\nYou are helpful.\n\nuser:\nHello\n\nassistant:\nHi there!"; - var messages = await _parser.ParseAsync(CreateAgent(), text); + var messages = await _parser.ParseAsync(CreateAgent(), text, null); Assert.Equal(3, messages.Count); Assert.Equal(Roles.System, messages[0].Role); Assert.Equal(Roles.User, messages[1].Role); @@ -35,7 +35,7 @@ public async Task Parse_MultipleRoles() [Fact] public async Task Parse_DeveloperRole() { - var messages = await _parser.ParseAsync(CreateAgent(), "developer:\nInstructions here."); + var messages = await _parser.ParseAsync(CreateAgent(), "developer:\nInstructions here.", null); Assert.Single(messages); Assert.Equal(Roles.Developer, messages[0].Role); } @@ -43,7 +43,7 @@ public async Task Parse_DeveloperRole() [Fact] public async Task Parse_ToolRole() { - var messages = await _parser.ParseAsync(CreateAgent(), "tool:\nTool response"); + var messages = await _parser.ParseAsync(CreateAgent(), "tool:\nTool response", null); Assert.Single(messages); Assert.Equal(Roles.Tool, messages[0].Role); } @@ -56,7 +56,7 @@ public async Task Parse_ToolRole() public async Task Parse_TrimsLeadingTrailingBlankLines() { var text = "system:\n\n\nHello\n\n"; - var messages = await _parser.ParseAsync(CreateAgent(), text); + var messages = await _parser.ParseAsync(CreateAgent(), text, null); Assert.Equal("Hello", messages[0].Text); } @@ -64,7 +64,7 @@ public async Task Parse_TrimsLeadingTrailingBlankLines() public async Task Parse_PreservesInternalWhitespace() { var text = "system:\nLine 1\n\nLine 3"; - var messages = await _parser.ParseAsync(CreateAgent(), text); + var messages = await _parser.ParseAsync(CreateAgent(), text, null); Assert.Contains("Line 1\n\nLine 3", messages[0].Text); } @@ -72,7 +72,7 @@ public async Task Parse_PreservesInternalWhitespace() public async Task Parse_MultilineContent() { var text = "system:\nLine 1\nLine 2\nLine 3"; - var messages = await _parser.ParseAsync(CreateAgent(), text); + var messages = await _parser.ParseAsync(CreateAgent(), text, null); Assert.Contains("Line 1", messages[0].Text); Assert.Contains("Line 3", messages[0].Text); } @@ -84,7 +84,7 @@ public async Task Parse_MultilineContent() [Fact] public async Task Parse_NoRoleMarkers_DefaultsToSystem() { - var messages = await _parser.ParseAsync(CreateAgent(), "Just some text"); + var messages = await _parser.ParseAsync(CreateAgent(), "Just some text", null); Assert.Single(messages); Assert.Equal(Roles.System, messages[0].Role); Assert.Equal("Just some text", messages[0].Text); @@ -93,14 +93,14 @@ public async Task Parse_NoRoleMarkers_DefaultsToSystem() [Fact] public async Task Parse_EmptyInput_ReturnsNoMessages() { - var messages = await _parser.ParseAsync(CreateAgent(), ""); + var messages = await _parser.ParseAsync(CreateAgent(), "", null); Assert.Empty(messages); } [Fact] public async Task Parse_WhitespaceOnly_ReturnsNoMessages() { - var messages = await _parser.ParseAsync(CreateAgent(), " \n\n "); + var messages = await _parser.ParseAsync(CreateAgent(), " \n\n ", null); Assert.Empty(messages); } @@ -112,7 +112,7 @@ public async Task Parse_WhitespaceOnly_ReturnsNoMessages() public async Task Parse_RoleWithAttributes() { var text = "tool[tool_call_id=\"call_123\", name=\"get_weather\"]:\nResult here"; - var messages = await _parser.ParseAsync(CreateAgent(), text); + var messages = await _parser.ParseAsync(CreateAgent(), text, null); Assert.Single(messages); Assert.Equal(Roles.Tool, messages[0].Role); Assert.Equal("call_123", messages[0].Metadata["tool_call_id"]); @@ -154,10 +154,10 @@ public async Task PreRender_Then_Parse_ValidatesNonce() var (sanitized, _) = parser.PreRender(template); // Parsing the pre-rendered template should work (nonce matches) - var messages = await parser.ParseAsync(CreateAgent(), sanitized); + var messages = await parser.ParseAsync(CreateAgent(), sanitized, null); Assert.Equal(2, messages.Count); // Nonce should NOT be in metadata - Assert.False(messages[0].Metadata.ContainsKey("nonce")); + Assert.False(messages[0].Metadata?.ContainsKey("nonce") ?? false); } // ----------------------------------------------------------------------- @@ -170,7 +170,7 @@ public async Task Parse_ThreadNonce_InContent() // Thread nonces appear in rendered text; parser should preserve them as-is // (thread expansion happens in pipeline, not parser) var text = "system:\nYou are helpful.\n__PROMPTY_THREAD_abcd1234_conversation__\nuser:\nHello"; - var messages = await _parser.ParseAsync(CreateAgent(), text); + var messages = await _parser.ParseAsync(CreateAgent(), text, null); Assert.Equal(2, messages.Count); Assert.Contains("__PROMPTY_THREAD_abcd1234_conversation__", messages[0].Text); } diff --git a/runtime/csharp/Prompty.Core.Tests/PipelineTests.cs b/runtime/csharp/Prompty.Core.Tests/PipelineTests.cs index 3b90abd4..cda1daa1 100644 --- a/runtime/csharp/Prompty.Core.Tests/PipelineTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/PipelineTests.cs @@ -323,7 +323,7 @@ public async Task RenderAsync_UsesFormatKind() public async Task ParseAsync_UsesParserKind() { var agent = CreateAgent(); - var messages = await Pipeline.ParseAsync(agent, "system:\nHello"); + var messages = await Pipeline.ParseAsync(agent, "system:\nHello", null); Assert.Single(messages); // MockParser returns 1 message } @@ -595,7 +595,7 @@ public Task RenderAsync(Prompty agent, string template, Dictionary> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) => Task.FromResult>([new Message { Role = Roles.System, Parts = [new TextPart { Value = rendered }] }]); } @@ -608,10 +608,10 @@ public List FormatToolMessages(object rawResponse, List toolC { var messages = new List { - new() { Role = Roles.Assistant, Parts = [], Metadata = new() { ["tool_calls"] = toolCalls } }, + new() { Role = Roles.Assistant, Parts = [], Metadata = new Dictionary { ["tool_calls"] = toolCalls } }, }; for (var i = 0; i < toolCalls.Count; i++) - messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new() { ["tool_call_id"] = toolCalls[i].Id } }); + messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new Dictionary { ["tool_call_id"] = toolCalls[i].Id } }); return messages; } } @@ -643,10 +643,10 @@ public List FormatToolMessages(object rawResponse, List toolC { var messages = new List { - new() { Role = Roles.Assistant, Parts = [], Metadata = new() { ["tool_calls"] = toolCalls } }, + new() { Role = Roles.Assistant, Parts = [], Metadata = new Dictionary { ["tool_calls"] = toolCalls } }, }; for (var i = 0; i < toolCalls.Count; i++) - messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new() { ["tool_call_id"] = toolCalls[i].Id } }); + messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new Dictionary { ["tool_call_id"] = toolCalls[i].Id } }); return messages; } } diff --git a/runtime/csharp/Prompty.Core.Tests/ResilienceTests.cs b/runtime/csharp/Prompty.Core.Tests/ResilienceTests.cs index eb498e89..769c34f3 100644 --- a/runtime/csharp/Prompty.Core.Tests/ResilienceTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/ResilienceTests.cs @@ -73,7 +73,7 @@ public Task RenderAsync(Prompty agent, string template, DictionaryA passthrough parser for resilience tests. file class PassthroughParser : IParser { - public Task> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) => Task.FromResult(new List { new() { Role = Roles.User, Parts = [new TextPart { Value = rendered }] } diff --git a/runtime/csharp/Prompty.Core.Tests/SpecVectorTests.cs b/runtime/csharp/Prompty.Core.Tests/SpecVectorTests.cs index e6b6590b..21a1e916 100644 --- a/runtime/csharp/Prompty.Core.Tests/SpecVectorTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/SpecVectorTests.cs @@ -151,7 +151,7 @@ public async Task ParseVectors_AllPass() try { - var messages = await parser.ParseAsync(agent, rendered); + var messages = await parser.ParseAsync(agent, rendered, null); var errors = CompareMessages(messages, expectedMessages); if (errors.Count > 0) diff --git a/runtime/csharp/Prompty.Core.Tests/StructuredOutputPipelineTests.cs b/runtime/csharp/Prompty.Core.Tests/StructuredOutputPipelineTests.cs index fab3f8e4..7f2d3333 100644 --- a/runtime/csharp/Prompty.Core.Tests/StructuredOutputPipelineTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/StructuredOutputPipelineTests.cs @@ -247,7 +247,7 @@ public Task RenderAsync(Prompty agent, string template, Dictionary internal class SingleMessageParser : IParser { - public Task> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) => Task.FromResult>( [new Message { Role = Roles.User, Parts = [new TextPart { Value = rendered }] }]); } @@ -265,7 +265,7 @@ public Task ExecuteAsync(Prompty agent, List messages) => Task.FromResult(_rawJson); public List FormatToolMessages(object rawResponse, List toolCalls, List toolResults, string? textContent = null) - => []; + => new List(); } /// diff --git a/runtime/csharp/Prompty.Core.Tests/TracingTests.cs b/runtime/csharp/Prompty.Core.Tests/TracingTests.cs index ffe611d0..e50d509b 100644 --- a/runtime/csharp/Prompty.Core.Tests/TracingTests.cs +++ b/runtime/csharp/Prompty.Core.Tests/TracingTests.cs @@ -571,7 +571,7 @@ public Task RenderAsync(Prompty agent, string template, Dictionary> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) => Task.FromResult>([new() { Role = Roles.User, Parts = [new TextPart { Value = rendered }] }]); } diff --git a/runtime/csharp/Prompty.Core/ContextWindow.cs b/runtime/csharp/Prompty.Core/ContextWindow.cs index 250fc6f7..bdba6ef5 100644 --- a/runtime/csharp/Prompty.Core/ContextWindow.cs +++ b/runtime/csharp/Prompty.Core/ContextWindow.cs @@ -19,7 +19,7 @@ public static int EstimateChars(List messages) else total += 200; } - if (msg.Metadata.TryGetValue("tool_calls", out var tc) && tc is not null) + if (msg.Metadata is not null && msg.Metadata.TryGetValue("tool_calls", out var tc) && tc is not null) total += System.Text.Json.JsonSerializer.Serialize(tc).Length; } return total; @@ -36,7 +36,7 @@ public static string SummarizeDropped(List messages) lines.Add($"User asked: {Truncate(msgText)}"); else if (msg.Role == "assistant") { - if (msg.Metadata.TryGetValue("tool_calls", out var toolCalls) && toolCalls is System.Collections.IEnumerable tcList) + if (msg.Metadata is not null && msg.Metadata.TryGetValue("tool_calls", out var toolCalls) && toolCalls is System.Collections.IEnumerable tcList) { var names = new List(); foreach (var tc in tcList) @@ -128,7 +128,7 @@ public static string FormatDroppedMessages(List messages) var lines = new List(); foreach (var msg in messages) { - if (msg.Metadata.TryGetValue("tool_calls", out var toolCalls) && toolCalls is System.Collections.IEnumerable tcList) + if (msg.Metadata is not null && msg.Metadata.TryGetValue("tool_calls", out var toolCalls) && toolCalls is System.Collections.IEnumerable tcList) { foreach (var tc in tcList) { diff --git a/runtime/csharp/Prompty.Core/Guardrails.cs b/runtime/csharp/Prompty.Core/Guardrails.cs index 23f3d792..d9d2319a 100644 --- a/runtime/csharp/Prompty.Core/Guardrails.cs +++ b/runtime/csharp/Prompty.Core/Guardrails.cs @@ -2,9 +2,6 @@ namespace Prompty.Core; -/// §13.4 Result of a guardrail check. -public record GuardrailResult(bool Allowed, string? Reason = null, object? Rewrite = null); - /// Error thrown when a guardrail denies the operation. public class GuardrailError : Exception { diff --git a/runtime/csharp/Prompty.Core/IPreRenderable.cs b/runtime/csharp/Prompty.Core/IPreRenderable.cs new file mode 100644 index 00000000..1dd994b1 --- /dev/null +++ b/runtime/csharp/Prompty.Core/IPreRenderable.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Prompty.Core; + +/// +/// Optional interface for parsers that can sanitize templates before rendering. +/// When implemented, the pipeline calls PreRender first to get a cleaned template +/// and context dict, then renders, then parses with that context. +/// +public interface IPreRenderable +{ + (string template, Dictionary context) PreRender(string template); +} diff --git a/runtime/csharp/Prompty.Core/Interfaces.cs b/runtime/csharp/Prompty.Core/Interfaces.cs deleted file mode 100644 index fd2efd3b..00000000 --- a/runtime/csharp/Prompty.Core/Interfaces.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Prompty.Core; - -/// -/// Renders a PromptAgent's instructions template with input values. -/// Registered by template format kind (e.g., "jinja2", "mustache"). -/// -public interface IRenderer -{ - Task RenderAsync(Prompty agent, string template, Dictionary inputs); -} - -/// -/// Parses rendered text into a list of Messages. -/// Registered by parser kind (e.g., "prompty"). -/// -public interface IParser -{ - Task> ParseAsync(Prompty agent, string rendered); -} - -/// -/// Optional interface for parsers that can sanitize templates before rendering. -/// When implemented, the pipeline calls PreRender first to get a cleaned template -/// and context dict, then renders, then parses with that context. -/// -public interface IPreRenderable -{ - (string template, Dictionary context) PreRender(string template); -} - -/// -/// Executes an LLM call with prepared messages. -/// Registered by provider name (e.g., "openai", "foundry", "anthropic"). -/// -public interface IExecutor -{ - Task ExecuteAsync(Prompty agent, List messages); - - /// - /// Formats the assistant tool-call response and dispatched tool results into - /// messages for the next agent loop iteration. Each provider implements this - /// to match its API's expected wire format, keeping the pipeline provider-agnostic. - /// - /// Original LLM response (for content block preservation). - /// Tool calls extracted by the processor. - /// Results from dispatching each tool call, parallel to toolCalls. - /// Any non-tool text content from the response. - List FormatToolMessages( - object rawResponse, - List toolCalls, - List toolResults, - string? textContent = null); -} - -/// -/// Post-processes raw LLM responses into a final result. -/// Registered by provider name (e.g., "openai", "foundry", "anthropic"). -/// -public interface IProcessor -{ - Task ProcessAsync(Prompty agent, object response); -} diff --git a/runtime/csharp/Prompty.Core/Model/AnonymousConnection.cs b/runtime/csharp/Prompty.Core/Model/AnonymousConnection.cs index ac41b2f0..67da5fec 100644 --- a/runtime/csharp/Prompty.Core/Model/AnonymousConnection.cs +++ b/runtime/csharp/Prompty.Core/Model/AnonymousConnection.cs @@ -7,9 +7,8 @@ namespace Prompty.Core; #pragma warning restore IDE0130 /// -/// /// -public class AnonymousConnection : Connection +public partial class AnonymousConnection : Connection { /// /// The shorthand property name for this type, if any. @@ -36,6 +35,7 @@ public AnonymousConnection() public string Endpoint { get; set; } = string.Empty; + #region Load Methods /// @@ -74,7 +74,6 @@ public AnonymousConnection() } - #endregion #region Save Methods @@ -97,15 +96,10 @@ public AnonymousConnection() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Endpoint is not null) - { - result["endpoint"] = obj.Endpoint; - } + + result["endpoint"] = obj.Endpoint; return result; diff --git a/runtime/csharp/Prompty.Core/Model/ApiKeyConnection.cs b/runtime/csharp/Prompty.Core/Model/ApiKeyConnection.cs index fcda3abe..c103ea25 100644 --- a/runtime/csharp/Prompty.Core/Model/ApiKeyConnection.cs +++ b/runtime/csharp/Prompty.Core/Model/ApiKeyConnection.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Connection configuration for AI services using API keys. /// -public class ApiKeyConnection : Connection +public partial class ApiKeyConnection : Connection { /// /// The shorthand property name for this type, if any. @@ -41,6 +41,7 @@ public ApiKeyConnection() public string ApiKey { get; set; } = string.Empty; + #region Load Methods /// @@ -84,7 +85,6 @@ public ApiKeyConnection() } - #endregion #region Save Methods @@ -107,20 +107,13 @@ public ApiKeyConnection() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Endpoint is not null) - { - result["endpoint"] = obj.Endpoint; - } - if (obj.ApiKey is not null) - { - result["apiKey"] = obj.ApiKey; - } + result["endpoint"] = obj.Endpoint; + + + result["apiKey"] = obj.ApiKey; return result; diff --git a/runtime/csharp/Prompty.Core/Model/ArrayProperty.cs b/runtime/csharp/Prompty.Core/Model/ArrayProperty.cs index 93aa7b7e..24d31f62 100644 --- a/runtime/csharp/Prompty.Core/Model/ArrayProperty.cs +++ b/runtime/csharp/Prompty.Core/Model/ArrayProperty.cs @@ -8,9 +8,10 @@ namespace Prompty.Core; /// /// Represents an array property. +/// /// This extends the base Property model to represent an array of items. /// -public class ArrayProperty : Property +public partial class ArrayProperty : Property { /// /// The shorthand property name for this type, if any. @@ -27,7 +28,7 @@ public ArrayProperty() #pragma warning restore CS8618 /// - /// + /// Kind /// public override string Kind { get; set; } = "array"; @@ -37,6 +38,7 @@ public ArrayProperty() public Property Items { get; set; } + #region Load Methods /// @@ -75,7 +77,6 @@ public ArrayProperty() } - #endregion #region Save Methods @@ -98,15 +99,10 @@ public ArrayProperty() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Items is not null) - { - result["items"] = obj.Items?.Save(context); - } + + result["items"] = obj.Items?.Save(context); return result; diff --git a/runtime/csharp/Prompty.Core/Model/AudioPart.cs b/runtime/csharp/Prompty.Core/Model/AudioPart.cs new file mode 100644 index 00000000..8e31fdf6 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/AudioPart.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// An audio content part. The source may be a URL or base64-encoded data. +/// +public partial class AudioPart : ContentPart +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public AudioPart() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for audio content + /// + public override string Kind { get; set; } = "audio"; + + /// + /// URL or base64-encoded audio data + /// + public string Source { get; set; } = string.Empty; + + /// + /// MIME type of the audio (e.g., 'audio/wav') + /// + public string? MediaType { get; set; } + + + + #region Load Methods + + /// + /// Load a AudioPart instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded AudioPart instance. + public new static AudioPart Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new AudioPart(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("source", out var sourceValue) && sourceValue is not null) + { + instance.Source = sourceValue?.ToString()!; + } + + if (data.TryGetValue("mediaType", out var mediaTypeValue) && mediaTypeValue is not null) + { + instance.MediaType = mediaTypeValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the AudioPart instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["source"] = obj.Source; + + + if (obj.MediaType is not null) + { + result["mediaType"] = obj.MediaType; + } + + + return result; + } + + + /// + /// Convert the AudioPart instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the AudioPart instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a AudioPart instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded AudioPart instance. + public new static AudioPart FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a AudioPart instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded AudioPart instance. + public new static AudioPart FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/Binding.cs b/runtime/csharp/Prompty.Core/Model/Binding.cs index ce9d737f..1990e8f3 100644 --- a/runtime/csharp/Prompty.Core/Model/Binding.cs +++ b/runtime/csharp/Prompty.Core/Model/Binding.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Represents a binding between an input property and a tool parameter. /// -public class Binding +public partial class Binding { /// /// The shorthand property name for this type, if any. @@ -36,6 +36,7 @@ public Binding() public string Input { get; set; } = string.Empty; + #region Load Methods /// @@ -53,7 +54,6 @@ public static Binding Load(Dictionary data, LoadContext? contex // Note: Alternate (shorthand) representations are handled by the converter - // Create new instance var instance = new Binding(); @@ -76,7 +76,6 @@ public static Binding Load(Dictionary data, LoadContext? contex } - #endregion #region Save Methods @@ -98,15 +97,10 @@ public static Binding Load(Dictionary data, LoadContext? contex var result = new Dictionary(); - if (obj.Name is not null) - { - result["name"] = obj.Name; - } + result["name"] = obj.Name; - if (obj.Input is not null) - { - result["input"] = obj.Input; - } + + result["input"] = obj.Input; if (context is not null) diff --git a/runtime/csharp/Prompty.Core/Model/CompactionCompletePayload.cs b/runtime/csharp/Prompty.Core/Model/CompactionCompletePayload.cs new file mode 100644 index 00000000..ab81e80c --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/CompactionCompletePayload.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "compaction_complete" events — context compaction finished. +/// +public partial class CompactionCompletePayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public CompactionCompletePayload() + { + } +#pragma warning restore CS8618 + + /// + /// Number of messages removed during compaction + /// + public int Removed { get; set; } + + /// + /// Number of messages remaining after compaction + /// + public int Remaining { get; set; } + + + + #region Load Methods + + /// + /// Load a CompactionCompletePayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionCompletePayload instance. + public static CompactionCompletePayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new CompactionCompletePayload(); + + + if (data.TryGetValue("removed", out var removedValue) && removedValue is not null) + { + instance.Removed = Convert.ToInt32(removedValue); + } + + if (data.TryGetValue("remaining", out var remainingValue) && remainingValue is not null) + { + instance.Remaining = Convert.ToInt32(remainingValue); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the CompactionCompletePayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["removed"] = obj.Removed; + + + result["remaining"] = obj.Remaining; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the CompactionCompletePayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the CompactionCompletePayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a CompactionCompletePayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionCompletePayload instance. + public static CompactionCompletePayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a CompactionCompletePayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionCompletePayload instance. + public static CompactionCompletePayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/CompactionConfig.cs b/runtime/csharp/Prompty.Core/Model/CompactionConfig.cs new file mode 100644 index 00000000..cd757c3e --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/CompactionConfig.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Configuration for context window compaction. When the message history +/// +/// exceeds the context budget, the compaction strategy is applied to +/// +/// reduce the message list while preserving essential information. +/// +public partial class CompactionConfig +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public CompactionConfig() + { + } +#pragma warning restore CS8618 + + /// + /// The compaction strategy identifier. Built-in strategies include 'summarize'. Can also be a path to a .prompty file used as the summarization prompt. + /// + public string? Strategy { get; set; } + + /// + /// Character budget for the compacted context. Overrides TurnOptions.contextBudget when set. + /// + public int? Budget { get; set; } + + /// + /// Additional strategy-specific options + /// + public IDictionary? Options { get; set; } + + + + #region Load Methods + + /// + /// Load a CompactionConfig instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionConfig instance. + public static CompactionConfig Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new CompactionConfig(); + + + if (data.TryGetValue("strategy", out var strategyValue) && strategyValue is not null) + { + instance.Strategy = strategyValue?.ToString()!; + } + + if (data.TryGetValue("budget", out var budgetValue) && budgetValue is not null) + { + instance.Budget = Convert.ToInt32(budgetValue); + } + + if (data.TryGetValue("options", out var optionsValue) && optionsValue is not null) + { + instance.Options = optionsValue.GetDictionary()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the CompactionConfig instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + if (obj.Strategy is not null) + { + result["strategy"] = obj.Strategy; + } + + + if (obj.Budget is not null) + { + result["budget"] = obj.Budget; + } + + + if (obj.Options is not null) + { + result["options"] = obj.Options; + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the CompactionConfig instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the CompactionConfig instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a CompactionConfig instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionConfig instance. + public static CompactionConfig FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a CompactionConfig instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionConfig instance. + public static CompactionConfig FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/CompactionFailedPayload.cs b/runtime/csharp/Prompty.Core/Model/CompactionFailedPayload.cs new file mode 100644 index 00000000..b6f8693c --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/CompactionFailedPayload.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "compaction_failed" events — compaction could not be completed. +/// +public partial class CompactionFailedPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public CompactionFailedPayload() + { + } +#pragma warning restore CS8618 + + /// + /// Explanation of why compaction failed + /// + public string Message { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a CompactionFailedPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionFailedPayload instance. + public static CompactionFailedPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new CompactionFailedPayload(); + + + if (data.TryGetValue("message", out var messageValue) && messageValue is not null) + { + instance.Message = messageValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the CompactionFailedPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["message"] = obj.Message; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the CompactionFailedPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the CompactionFailedPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a CompactionFailedPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionFailedPayload instance. + public static CompactionFailedPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a CompactionFailedPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded CompactionFailedPayload instance. + public static CompactionFailedPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/Connection.cs b/runtime/csharp/Prompty.Core/Model/Connection.cs index d82a7512..4deaed81 100644 --- a/runtime/csharp/Prompty.Core/Model/Connection.cs +++ b/runtime/csharp/Prompty.Core/Model/Connection.cs @@ -8,10 +8,12 @@ namespace Prompty.Core; /// /// Connection configuration for AI agents. +/// /// `provider`, `kind`, and `endpoint` are required properties here, +/// /// but this section can accept additional via options. /// -public abstract class Connection +public abstract partial class Connection { /// /// The shorthand property name for this type, if any. @@ -43,6 +45,7 @@ protected Connection() public string? UsageDescription { get; set; } + #region Load Methods /// @@ -86,7 +89,6 @@ public static Connection Load(Dictionary data, LoadContext? con } - /// /// Load polymorphic Connection based on discriminator. /// @@ -133,16 +135,15 @@ private static Connection LoadKind(Dictionary data, LoadContext var result = new Dictionary(); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + if (obj.AuthenticationMode is not null) { result["authenticationMode"] = obj.AuthenticationMode; } + if (obj.UsageDescription is not null) { result["usageDescription"] = obj.UsageDescription; diff --git a/runtime/csharp/Prompty.Core/Model/ContentPart.cs b/runtime/csharp/Prompty.Core/Model/ContentPart.cs new file mode 100644 index 00000000..8bf55070 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ContentPart.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A part of a message's content. Content parts are discriminated on the `kind` +/// +/// field and represent the different modalities that can appear in a message. +/// +public abstract partial class ContentPart +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + protected ContentPart() + { + } +#pragma warning restore CS8618 + + /// + /// The kind of content part + /// + public virtual string Kind { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ContentPart instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ContentPart instance. + public static ContentPart Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Load polymorphic ContentPart instance + var instance = LoadKind(data, context); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load polymorphic ContentPart based on discriminator. + /// + private static ContentPart LoadKind(Dictionary data, LoadContext? context) + { + if (data.TryGetValue("kind", out var discriminatorValue) && discriminatorValue is not null) + { + var discriminator = discriminatorValue.ToString()?.ToLowerInvariant(); + return discriminator switch + { + "text" => TextPart.Load(data, context), + "image" => ImagePart.Load(data, context), + "file" => FilePart.Load(data, context), + "audio" => AudioPart.Load(data, context), + _ => throw new ArgumentException($"Unknown ContentPart discriminator value: {discriminator}"), + }; + } + + throw new ArgumentException("Missing ContentPart discriminator property: 'kind'"); + + } + + + #endregion + + #region Save Methods + + /// + /// Save the ContentPart instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public virtual Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["kind"] = obj.Kind; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ContentPart instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ContentPart instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ContentPart instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ContentPart instance. + public static ContentPart FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ContentPart instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ContentPart instance. + public static ContentPart FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/CustomTool.cs b/runtime/csharp/Prompty.Core/Model/CustomTool.cs index dc6f88ba..f6cef50c 100644 --- a/runtime/csharp/Prompty.Core/Model/CustomTool.cs +++ b/runtime/csharp/Prompty.Core/Model/CustomTool.cs @@ -8,12 +8,16 @@ namespace Prompty.Core; /// /// Represents a generic server tool that runs on a server +/// /// This tool kind is designed for operations that require server-side execution +/// /// It may include features such as authentication, data storage, and long-running processes +/// /// This tool kind is ideal for tasks that involve complex computations or access to secure resources +/// /// Server tools can be used to offload heavy processing from client applications /// -public class CustomTool : Tool +public partial class CustomTool : Tool { /// /// The shorthand property name for this type, if any. @@ -45,6 +49,7 @@ public CustomTool() public IDictionary Options { get; set; } = new Dictionary(); + #region Load Methods /// @@ -88,7 +93,6 @@ public CustomTool() } - #endregion #region Save Methods @@ -111,20 +115,13 @@ public CustomTool() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Connection is not null) - { - result["connection"] = obj.Connection?.Save(context); - } - if (obj.Options is not null) - { - result["options"] = obj.Options; - } + result["connection"] = obj.Connection?.Save(context); + + + result["options"] = obj.Options; return result; diff --git a/runtime/csharp/Prompty.Core/Model/DoneEventPayload.cs b/runtime/csharp/Prompty.Core/Model/DoneEventPayload.cs new file mode 100644 index 00000000..3845e84b --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/DoneEventPayload.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "done" events — the agent loop completed successfully. +/// +public partial class DoneEventPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public DoneEventPayload() + { + } +#pragma warning restore CS8618 + + /// + /// The final text response from the LLM + /// + public string Response { get; set; } = string.Empty; + + /// + /// The final conversation state including all messages + /// + public IList Messages { get; set; } = []; + + + + #region Load Methods + + /// + /// Load a DoneEventPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded DoneEventPayload instance. + public static DoneEventPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new DoneEventPayload(); + + + if (data.TryGetValue("response", out var responseValue) && responseValue is not null) + { + instance.Response = responseValue?.ToString()!; + } + + if (data.TryGetValue("messages", out var messagesValue) && messagesValue is not null) + { + instance.Messages = LoadMessages(messagesValue, context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load a list of Message from a dictionary or list. + /// + public static IList LoadMessages(object data, LoadContext? context) + { + var result = new List(); + + if (data is Dictionary dict) + { + // Convert named dictionary to list + foreach (var kvp in dict) + { + if (kvp.Value is IEnumerable) + { + throw new ArgumentException( + $"Invalid 'messages' format: key '{kvp.Key}' has an array value. " + + $"'messages' must be a flat list of objects or a name-keyed dict — " + + "not a nested {" + kvp.Key + ": [...]} structure."); + } + var itemDict = kvp.Value.GetDictionary(); + if (itemDict.Count > 0) + { + // Value is an object, add name to it + itemDict["name"] = kvp.Key; + result.Add(Message.Load(itemDict, context)); + } + else + { + // Value is a scalar, use it as the primary property + var newDict = new Dictionary + { + ["name"] = kvp.Key, + ["role"] = kvp.Value + }; + result.Add(Message.Load(newDict, context)); + } + } + } + else if (data is IEnumerable list) + { + foreach (var item in list) + { + var itemDict = item.GetDictionary(Message.ShorthandProperty); + if (itemDict.Count > 0) + { + result.Add(Message.Load(itemDict, context)); + } + } + } + + return result; + } + + + #endregion + + #region Save Methods + + /// + /// Save the DoneEventPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["response"] = obj.Response; + + + result["messages"] = SaveMessages(obj.Messages, context); + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Save a list of Message to object or array format. + /// + public static object SaveMessages(IList items, SaveContext? context) + { + context ??= new SaveContext(); + + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); + + } + + + /// + /// Convert the DoneEventPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the DoneEventPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a DoneEventPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded DoneEventPayload instance. + public static DoneEventPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a DoneEventPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded DoneEventPayload instance. + public static DoneEventPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ErrorChunk.cs b/runtime/csharp/Prompty.Core/Model/ErrorChunk.cs new file mode 100644 index 00000000..430616bf --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ErrorChunk.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// An error chunk from the LLM response stream. +/// +public partial class ErrorChunk : StreamChunk +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ErrorChunk() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for error chunks + /// + public override string Kind { get; set; } = "error"; + + /// + /// The error message + /// + public string Message { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ErrorChunk instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ErrorChunk instance. + public new static ErrorChunk Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ErrorChunk(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("message", out var messageValue) && messageValue is not null) + { + instance.Message = messageValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ErrorChunk instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["message"] = obj.Message; + + + return result; + } + + + /// + /// Convert the ErrorChunk instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ErrorChunk instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ErrorChunk instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ErrorChunk instance. + public new static ErrorChunk FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ErrorChunk instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ErrorChunk instance. + public new static ErrorChunk FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ErrorEventPayload.cs b/runtime/csharp/Prompty.Core/Model/ErrorEventPayload.cs new file mode 100644 index 00000000..8513e878 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ErrorEventPayload.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "error" events — an error occurred during the loop. +/// +public partial class ErrorEventPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ErrorEventPayload() + { + } +#pragma warning restore CS8618 + + /// + /// Human-readable error description + /// + public string Message { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ErrorEventPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ErrorEventPayload instance. + public static ErrorEventPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ErrorEventPayload(); + + + if (data.TryGetValue("message", out var messageValue) && messageValue is not null) + { + instance.Message = messageValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ErrorEventPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["message"] = obj.Message; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ErrorEventPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ErrorEventPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ErrorEventPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ErrorEventPayload instance. + public static ErrorEventPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ErrorEventPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ErrorEventPayload instance. + public static ErrorEventPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/Executor.cs b/runtime/csharp/Prompty.Core/Model/Executor.cs new file mode 100644 index 00000000..3ed3cd9b --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/Executor.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Calls an LLM provider with messages and returns the raw provider response. +/// +public interface IExecutor +{ + /// + /// Call an LLM provider with messages and return the raw response + /// + Task ExecuteAsync(Prompty agent, List messages); + /// + /// Call an LLM provider and return a streaming response. Returns a language-specific async iterable/stream of raw chunks. Not all providers support streaming; the default implementation should signal lack of support. + /// + Task ExecuteStreamAsync(Prompty agent, List messages) => Task.FromResult(default); + /// + /// Format tool call results into messages for the next iteration + /// + List FormatToolMessages(object rawResponse, List toolCalls, List toolResults, string? textContent); +} diff --git a/runtime/csharp/Prompty.Core/Model/FilePart.cs b/runtime/csharp/Prompty.Core/Model/FilePart.cs new file mode 100644 index 00000000..14082700 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/FilePart.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A file content part. The source may be a URL or base64-encoded data. +/// +public partial class FilePart : ContentPart +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public FilePart() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for file content + /// + public override string Kind { get; set; } = "file"; + + /// + /// URL or base64-encoded file data + /// + public string Source { get; set; } = string.Empty; + + /// + /// MIME type of the file (e.g., 'application/pdf') + /// + public string? MediaType { get; set; } + + + + #region Load Methods + + /// + /// Load a FilePart instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded FilePart instance. + public new static FilePart Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new FilePart(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("source", out var sourceValue) && sourceValue is not null) + { + instance.Source = sourceValue?.ToString()!; + } + + if (data.TryGetValue("mediaType", out var mediaTypeValue) && mediaTypeValue is not null) + { + instance.MediaType = mediaTypeValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the FilePart instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["source"] = obj.Source; + + + if (obj.MediaType is not null) + { + result["mediaType"] = obj.MediaType; + } + + + return result; + } + + + /// + /// Convert the FilePart instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the FilePart instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a FilePart instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded FilePart instance. + public new static FilePart FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a FilePart instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded FilePart instance. + public new static FilePart FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/FormatConfig.cs b/runtime/csharp/Prompty.Core/Model/FormatConfig.cs index 4fe7ed5d..7eeab289 100644 --- a/runtime/csharp/Prompty.Core/Model/FormatConfig.cs +++ b/runtime/csharp/Prompty.Core/Model/FormatConfig.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Template format definition /// -public class FormatConfig +public partial class FormatConfig { /// /// The shorthand property name for this type, if any. @@ -41,6 +41,7 @@ public FormatConfig() public IDictionary? Options { get; set; } + #region Load Methods /// @@ -58,7 +59,6 @@ public static FormatConfig Load(Dictionary data, LoadContext? c // Note: Alternate (shorthand) representations are handled by the converter - // Create new instance var instance = new FormatConfig(); @@ -86,7 +86,6 @@ public static FormatConfig Load(Dictionary data, LoadContext? c } - #endregion #region Save Methods @@ -108,16 +107,15 @@ public static FormatConfig Load(Dictionary data, LoadContext? c var result = new Dictionary(); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + if (obj.Strict is not null) { result["strict"] = obj.Strict; } + if (obj.Options is not null) { result["options"] = obj.Options; diff --git a/runtime/csharp/Prompty.Core/Model/FoundryConnection.cs b/runtime/csharp/Prompty.Core/Model/FoundryConnection.cs index 2402c861..5be78291 100644 --- a/runtime/csharp/Prompty.Core/Model/FoundryConnection.cs +++ b/runtime/csharp/Prompty.Core/Model/FoundryConnection.cs @@ -8,10 +8,12 @@ namespace Prompty.Core; /// /// Connection configuration for Microsoft Foundry projects. +/// /// Provides project-scoped access to models, tools, and services +/// /// via Entra ID (DefaultAzureCredential) authentication. /// -public class FoundryConnection : Connection +public partial class FoundryConnection : Connection { /// /// The shorthand property name for this type, if any. @@ -48,6 +50,7 @@ public FoundryConnection() public string? ConnectionType { get; set; } + #region Load Methods /// @@ -96,7 +99,6 @@ public FoundryConnection() } - #endregion #region Save Methods @@ -119,21 +121,18 @@ public FoundryConnection() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + + + result["endpoint"] = obj.Endpoint; - if (obj.Endpoint is not null) - { - result["endpoint"] = obj.Endpoint; - } if (obj.Name is not null) { result["name"] = obj.Name; } + if (obj.ConnectionType is not null) { result["connectionType"] = obj.ConnectionType; diff --git a/runtime/csharp/Prompty.Core/Model/FunctionTool.cs b/runtime/csharp/Prompty.Core/Model/FunctionTool.cs index 6ff23ec5..6a8bb165 100644 --- a/runtime/csharp/Prompty.Core/Model/FunctionTool.cs +++ b/runtime/csharp/Prompty.Core/Model/FunctionTool.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Represents a local function tool. /// -public class FunctionTool : Tool +public partial class FunctionTool : Tool { /// /// The shorthand property name for this type, if any. @@ -41,6 +41,7 @@ public FunctionTool() public bool? Strict { get; set; } + #region Load Methods /// @@ -138,7 +139,6 @@ public static IList LoadParameters(object data, LoadContext? context) } - #endregion #region Save Methods @@ -161,15 +161,11 @@ public static IList LoadParameters(object data, LoadContext? context) var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + + + result["parameters"] = SaveParameters(obj.Parameters, context); - if (obj.Parameters is not null) - { - result["parameters"] = SaveParameters(obj.Parameters, context); - } if (obj.Strict is not null) { @@ -188,39 +184,8 @@ public static object SaveParameters(IList items, SaveContext? context) { context ??= new SaveContext(); - - if (context.CollectionFormat == "array") - { - return items.Select(item => item.Save(context)).ToList(); - } - - // Object format: use name as key - var result = new Dictionary(); - foreach (var item in items) - { - var itemData = item.Save(context); - if (itemData.TryGetValue("name", out var nameValue) && nameValue is string name) - { - itemData.Remove("name"); - - // Check if we can use shorthand - if (context.UseShorthand && Property.ShorthandProperty is string shorthandProp) - { - if (itemData.Count == 1 && itemData.ContainsKey(shorthandProp)) - { - result[name] = itemData[shorthandProp]; - continue; - } - } - result[name] = itemData; - } - else - { - // No name, can't use object format for this item - throw new InvalidOperationException("Cannot save item in object format: missing 'name' property"); - } - } - return result; + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); } diff --git a/runtime/csharp/Prompty.Core/Model/GuardrailResult.cs b/runtime/csharp/Prompty.Core/Model/GuardrailResult.cs new file mode 100644 index 00000000..bd8b00c9 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/GuardrailResult.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// The result of a guardrail evaluation. Guardrails are safety checks that +/// +/// run at specific phases of the agent loop and can allow, deny, or rewrite +/// +/// content. +/// +public partial class GuardrailResult +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public GuardrailResult() + { + } +#pragma warning restore CS8618 + + /// + /// Whether the content passed the guardrail check + /// + public bool Allowed { get; set; } = false; + + /// + /// Explanation of why the content was allowed or denied + /// + public string? Reason { get; set; } + + /// + /// Optional rewritten content to replace the original + /// + public object? Rewrite { get; set; } + + + + #region Load Methods + + /// + /// Load a GuardrailResult instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded GuardrailResult instance. + public static GuardrailResult Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new GuardrailResult(); + + + if (data.TryGetValue("allowed", out var allowedValue) && allowedValue is not null) + { + instance.Allowed = Convert.ToBoolean(allowedValue); + } + + if (data.TryGetValue("reason", out var reasonValue) && reasonValue is not null) + { + instance.Reason = reasonValue?.ToString()!; + } + + if (data.TryGetValue("rewrite", out var rewriteValue) && rewriteValue is not null) + { + instance.Rewrite = rewriteValue; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the GuardrailResult instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["allowed"] = obj.Allowed; + + + if (obj.Reason is not null) + { + result["reason"] = obj.Reason; + } + + + if (obj.Rewrite is not null) + { + result["rewrite"] = obj.Rewrite; + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the GuardrailResult instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the GuardrailResult instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a GuardrailResult instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded GuardrailResult instance. + public static GuardrailResult FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a GuardrailResult instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded GuardrailResult instance. + public static GuardrailResult FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion + + #region Factory Methods + + /// + /// Create a GuardrailResult with preset field values. + /// + public static GuardrailResult CreateRewrite(object? rewrite) + { + return new GuardrailResult { Allowed = true, Rewrite = rewrite }; + } + + /// + /// Create a GuardrailResult with preset field values. + /// + public static GuardrailResult Deny(string reason) + { + return new GuardrailResult { Allowed = false, Reason = reason }; + } + + /// + /// Create a GuardrailResult with preset field values. + /// + public static GuardrailResult Allow() + { + return new GuardrailResult { Allowed = true }; + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/GuardrailResultExtensions.cs b/runtime/csharp/Prompty.Core/Model/GuardrailResultExtensions.cs new file mode 100644 index 00000000..9b8cb904 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/GuardrailResultExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +public partial class GuardrailResult +{ + /// + /// Convenience constructor matching the old record signature. + /// + public GuardrailResult(bool allowed, string? reason = null, object? rewrite = null) + { + Allowed = allowed; + Reason = reason; + Rewrite = rewrite; + } +} diff --git a/runtime/csharp/Prompty.Core/Model/ImagePart.cs b/runtime/csharp/Prompty.Core/Model/ImagePart.cs new file mode 100644 index 00000000..db67e64e --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ImagePart.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// An image content part. The source may be a URL or base64-encoded data. +/// +public partial class ImagePart : ContentPart +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ImagePart() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for image content + /// + public override string Kind { get; set; } = "image"; + + /// + /// URL or base64-encoded image data + /// + public string Source { get; set; } = string.Empty; + + /// + /// Detail level hint for the model (e.g., 'auto', 'low', 'high') + /// + public string? Detail { get; set; } + + /// + /// MIME type of the image (e.g., 'image/png') + /// + public string? MediaType { get; set; } + + + + #region Load Methods + + /// + /// Load a ImagePart instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ImagePart instance. + public new static ImagePart Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ImagePart(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("source", out var sourceValue) && sourceValue is not null) + { + instance.Source = sourceValue?.ToString()!; + } + + if (data.TryGetValue("detail", out var detailValue) && detailValue is not null) + { + instance.Detail = detailValue?.ToString()!; + } + + if (data.TryGetValue("mediaType", out var mediaTypeValue) && mediaTypeValue is not null) + { + instance.MediaType = mediaTypeValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ImagePart instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["source"] = obj.Source; + + + if (obj.Detail is not null) + { + result["detail"] = obj.Detail; + } + + + if (obj.MediaType is not null) + { + result["mediaType"] = obj.MediaType; + } + + + return result; + } + + + /// + /// Convert the ImagePart instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ImagePart instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ImagePart instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ImagePart instance. + public new static ImagePart FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ImagePart instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ImagePart instance. + public new static ImagePart FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/McpApprovalMode.cs b/runtime/csharp/Prompty.Core/Model/McpApprovalMode.cs index f7e7716b..d281c400 100644 --- a/runtime/csharp/Prompty.Core/Model/McpApprovalMode.cs +++ b/runtime/csharp/Prompty.Core/Model/McpApprovalMode.cs @@ -8,10 +8,12 @@ namespace Prompty.Core; /// /// The approval mode for MCP server tools. -/// When kind is "specify", use alwaysRequireApprovalTools and neverRequireApprovalTools -/// to control per-tool approval. For "always" and "never", those fields are ignored. +/// +/// When kind is "specify", use alwaysRequireApprovalTools and neverRequireApprovalTools +/// +/// to control per-tool approval. For "always" and "never", those fields are ignored. /// -public class McpApprovalMode +public partial class McpApprovalMode { /// /// The shorthand property name for this type, if any. @@ -43,6 +45,7 @@ public McpApprovalMode() public IList? NeverRequireApprovalTools { get; set; } + #region Load Methods /// @@ -60,7 +63,6 @@ public static McpApprovalMode Load(Dictionary data, LoadContext // Note: Alternate (shorthand) representations are handled by the converter - // Create new instance var instance = new McpApprovalMode(); @@ -88,7 +90,6 @@ public static McpApprovalMode Load(Dictionary data, LoadContext } - #endregion #region Save Methods @@ -110,16 +111,15 @@ public static McpApprovalMode Load(Dictionary data, LoadContext var result = new Dictionary(); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + if (obj.AlwaysRequireApprovalTools is not null) { result["alwaysRequireApprovalTools"] = obj.AlwaysRequireApprovalTools; } + if (obj.NeverRequireApprovalTools is not null) { result["neverRequireApprovalTools"] = obj.NeverRequireApprovalTools; diff --git a/runtime/csharp/Prompty.Core/Model/McpTool.cs b/runtime/csharp/Prompty.Core/Model/McpTool.cs index 656a1c4a..8ce8e077 100644 --- a/runtime/csharp/Prompty.Core/Model/McpTool.cs +++ b/runtime/csharp/Prompty.Core/Model/McpTool.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// The MCP Server tool. /// -public class McpTool : Tool +public partial class McpTool : Tool { /// /// The shorthand property name for this type, if any. @@ -56,6 +56,7 @@ public McpTool() public IList? AllowedTools { get; set; } + #region Load Methods /// @@ -114,7 +115,6 @@ public McpTool() } - #endregion #region Save Methods @@ -137,30 +137,23 @@ public McpTool() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Connection is not null) - { - result["connection"] = obj.Connection?.Save(context); - } - if (obj.ServerName is not null) - { - result["serverName"] = obj.ServerName; - } + result["connection"] = obj.Connection?.Save(context); + + + result["serverName"] = obj.ServerName; + if (obj.ServerDescription is not null) { result["serverDescription"] = obj.ServerDescription; } - if (obj.ApprovalMode is not null) - { - result["approvalMode"] = obj.ApprovalMode?.Save(context); - } + + result["approvalMode"] = obj.ApprovalMode?.Save(context); + if (obj.AllowedTools is not null) { diff --git a/runtime/csharp/Prompty.Core/Model/Message.cs b/runtime/csharp/Prompty.Core/Model/Message.cs new file mode 100644 index 00000000..288e70b3 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/Message.cs @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A message in a conversation. Messages have a role and a list of content parts +/// +/// representing the different modalities of the message content. +/// +public partial class Message : IMessageHelpers +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public Message() + { + } +#pragma warning restore CS8618 + + /// + /// The role of the message sender + /// + public string Role { get; set; } = "user"; + + /// + /// The content parts of the message + /// + public IList Parts { get; set; } = []; + + /// + /// Optional metadata associated with the message + /// + public IDictionary Metadata { get; set; } = new Dictionary(); + + + + #region Load Methods + + /// + /// Load a Message instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded Message instance. + public static Message Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new Message(); + + + if (data.TryGetValue("role", out var roleValue) && roleValue is not null) + { + instance.Role = roleValue?.ToString()!; + } + + if (data.TryGetValue("parts", out var partsValue) && partsValue is not null) + { + instance.Parts = LoadParts(partsValue, context); + } + + if (data.TryGetValue("metadata", out var metadataValue) && metadataValue is not null) + { + instance.Metadata = metadataValue.GetDictionary()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load a list of ContentPart from a dictionary or list. + /// + public static IList LoadParts(object data, LoadContext? context) + { + var result = new List(); + + if (data is Dictionary dict) + { + // Convert named dictionary to list + foreach (var kvp in dict) + { + if (kvp.Value is IEnumerable) + { + throw new ArgumentException( + $"Invalid 'parts' format: key '{kvp.Key}' has an array value. " + + $"'parts' must be a flat list of objects or a name-keyed dict — " + + "not a nested {" + kvp.Key + ": [...]} structure."); + } + var itemDict = kvp.Value.GetDictionary(); + if (itemDict.Count > 0) + { + // Value is an object, add name to it + itemDict["name"] = kvp.Key; + result.Add(ContentPart.Load(itemDict, context)); + } + else + { + // Value is a scalar, use it as the primary property + var newDict = new Dictionary + { + ["name"] = kvp.Key, + ["kind"] = kvp.Value + }; + result.Add(ContentPart.Load(newDict, context)); + } + } + } + else if (data is IEnumerable list) + { + foreach (var item in list) + { + var itemDict = item.GetDictionary(ContentPart.ShorthandProperty); + if (itemDict.Count > 0) + { + result.Add(ContentPart.Load(itemDict, context)); + } + } + } + + return result; + } + + + #endregion + + #region Save Methods + + /// + /// Save the Message instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["role"] = obj.Role; + + + result["parts"] = SaveParts(obj.Parts, context); + + + result["metadata"] = obj.Metadata; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Save a list of ContentPart to object or array format. + /// + public static object SaveParts(IList items, SaveContext? context) + { + context ??= new SaveContext(); + + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); + + } + + + /// + /// Convert the Message instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the Message instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a Message instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded Message instance. + public static Message FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a Message instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded Message instance. + public static Message FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion + + #region Factory Methods + + /// + /// Create a Message with preset field values. + /// + public static Message Assistant(string text) + { + return new Message { Role = "assistant", Parts = new List { new TextPart { Value = text } } }; + } + + /// + /// Create a Message with preset field values. + /// + public static Message System(string text) + { + return new Message { Role = "system", Parts = new List { new TextPart { Value = text } } }; + } + + /// + /// Create a Message with preset field values. + /// + public static Message User(string text) + { + return new Message { Role = "user", Parts = new List { new TextPart { Value = text } } }; + } + + #endregion +} + +/// +/// Helper contract for . +/// +/// Runtime implementations must provide these members on Message (via a +/// hand-written partial class). The C# compiler enforces conformance +/// because Message declares : IMessageHelpers. +/// +public partial interface IMessageHelpers +{ + /// + /// Return plain string if all parts are text, else a list of content part dicts for wire serialization + /// + object ToTextContent(); + /// + /// Concatenate all TextPart values joined by newline + /// + string Text { get; } +} diff --git a/runtime/csharp/Prompty.Core/Model/MessageHelpers.cs b/runtime/csharp/Prompty.Core/Model/MessageHelpers.cs new file mode 100644 index 00000000..da9fd04c --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/MessageHelpers.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +// --- Runtime helpers (manually maintained) --- +// This file extends the generated Message class with convenience members +// used by the Prompty pipeline and wire-format converters. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +public partial class Message +{ + /// + /// Concatenated text from all TextParts. + /// + public string Text => string.Join("", Parts.OfType().Select(p => p.Value)); + + /// + /// Returns the content as a simple string if only text parts exist, + /// or as a list of wire-format dictionaries for multimodal content. + /// + public object ToTextContent() + { + if (Parts.All(p => p is TextPart)) + return Text; + + return Parts.Select>(p => p switch + { + TextPart t => new() { ["type"] = "text", ["text"] = t.Value }, + ImagePart i => new() + { + ["type"] = "image_url", + ["image_url"] = new Dictionary + { + ["url"] = i.Source, + ["detail"] = i.Detail ?? "auto", + }, + }, + FilePart f => new() { ["type"] = "file", ["file"] = new Dictionary { ["url"] = f.Source } }, + AudioPart a => new() { ["type"] = "input_audio", ["input_audio"] = new Dictionary { ["url"] = a.Source } }, + _ => new() { ["type"] = p.Kind }, + }).ToList(); + } +} diff --git a/runtime/csharp/Prompty.Core/Model/MessagesUpdatedPayload.cs b/runtime/csharp/Prompty.Core/Model/MessagesUpdatedPayload.cs new file mode 100644 index 00000000..cfddbe1a --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/MessagesUpdatedPayload.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "messages_updated" events — the conversation state has changed. +/// +public partial class MessagesUpdatedPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public MessagesUpdatedPayload() + { + } +#pragma warning restore CS8618 + + /// + /// The current full message list after the update + /// + public IList Messages { get; set; } = []; + + + + #region Load Methods + + /// + /// Load a MessagesUpdatedPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded MessagesUpdatedPayload instance. + public static MessagesUpdatedPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new MessagesUpdatedPayload(); + + + if (data.TryGetValue("messages", out var messagesValue) && messagesValue is not null) + { + instance.Messages = LoadMessages(messagesValue, context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load a list of Message from a dictionary or list. + /// + public static IList LoadMessages(object data, LoadContext? context) + { + var result = new List(); + + if (data is Dictionary dict) + { + // Convert named dictionary to list + foreach (var kvp in dict) + { + if (kvp.Value is IEnumerable) + { + throw new ArgumentException( + $"Invalid 'messages' format: key '{kvp.Key}' has an array value. " + + $"'messages' must be a flat list of objects or a name-keyed dict — " + + "not a nested {" + kvp.Key + ": [...]} structure."); + } + var itemDict = kvp.Value.GetDictionary(); + if (itemDict.Count > 0) + { + // Value is an object, add name to it + itemDict["name"] = kvp.Key; + result.Add(Message.Load(itemDict, context)); + } + else + { + // Value is a scalar, use it as the primary property + var newDict = new Dictionary + { + ["name"] = kvp.Key, + ["role"] = kvp.Value + }; + result.Add(Message.Load(newDict, context)); + } + } + } + else if (data is IEnumerable list) + { + foreach (var item in list) + { + var itemDict = item.GetDictionary(Message.ShorthandProperty); + if (itemDict.Count > 0) + { + result.Add(Message.Load(itemDict, context)); + } + } + } + + return result; + } + + + #endregion + + #region Save Methods + + /// + /// Save the MessagesUpdatedPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["messages"] = SaveMessages(obj.Messages, context); + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Save a list of Message to object or array format. + /// + public static object SaveMessages(IList items, SaveContext? context) + { + context ??= new SaveContext(); + + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); + + } + + + /// + /// Convert the MessagesUpdatedPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the MessagesUpdatedPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a MessagesUpdatedPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded MessagesUpdatedPayload instance. + public static MessagesUpdatedPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a MessagesUpdatedPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded MessagesUpdatedPayload instance. + public static MessagesUpdatedPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/Model.cs b/runtime/csharp/Prompty.Core/Model/Model.cs index 68fcbd53..26ed0117 100644 --- a/runtime/csharp/Prompty.Core/Model/Model.cs +++ b/runtime/csharp/Prompty.Core/Model/Model.cs @@ -8,10 +8,12 @@ namespace Prompty.Core; /// /// Model for defining the structure and behavior of AI agents. -/// This model includes properties for specifying the model's provider, connection details, and various options. +/// +/// This model includes properties for specifying the model's provider, connection details, and various options. +/// /// It allows for flexible configuration of AI models to suit different use cases and requirements. /// -public class Model +public partial class Model { /// /// The shorthand property name for this type, if any. @@ -53,6 +55,7 @@ public Model() public ModelOptions? Options { get; set; } + #region Load Methods /// @@ -70,7 +73,6 @@ public static Model Load(Dictionary data, LoadContext? context // Note: Alternate (shorthand) representations are handled by the converter - // Create new instance var instance = new Model(); @@ -108,7 +110,6 @@ public static Model Load(Dictionary data, LoadContext? context } - #endregion #region Save Methods @@ -130,26 +131,27 @@ public static Model Load(Dictionary data, LoadContext? context var result = new Dictionary(); - if (obj.Id is not null) - { - result["id"] = obj.Id; - } + result["id"] = obj.Id; + if (obj.Provider is not null) { result["provider"] = obj.Provider; } + if (obj.ApiType is not null) { result["apiType"] = obj.ApiType; } + if (obj.Connection is not null) { result["connection"] = obj.Connection?.Save(context); } + if (obj.Options is not null) { result["options"] = obj.Options?.Save(context); diff --git a/runtime/csharp/Prompty.Core/Model/ModelInfo.cs b/runtime/csharp/Prompty.Core/Model/ModelInfo.cs new file mode 100644 index 00000000..14620536 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ModelInfo.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Information about a model available from a provider. Used by provider-level +/// +/// model discovery to report which models are available and their capabilities. +/// +/// Not all providers return all fields — implementations SHOULD populate as +/// +/// many fields as the provider's API supports and MAY enrich sparse results +/// +/// from a built-in lookup table of known models. +/// +public partial class ModelInfo +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ModelInfo() + { + } +#pragma warning restore CS8618 + + /// + /// The model identifier (e.g., 'gpt-4o', 'claude-3-opus') + /// + public string Id { get; set; } = string.Empty; + + /// + /// Human-readable display name + /// + public string? DisplayName { get; set; } + + /// + /// The organization or entity that owns the model + /// + public string? OwnedBy { get; set; } + + /// + /// Maximum context window size in tokens + /// + public int? ContextWindow { get; set; } + + /// + /// Input modalities the model accepts (e.g., 'text', 'image', 'audio') + /// + public IList? InputModalities { get; set; } + + /// + /// Output modalities the model can produce (e.g., 'text', 'audio') + /// + public IList? OutputModalities { get; set; } + + /// + /// Additional provider-specific properties + /// + public IDictionary? AdditionalProperties { get; set; } + + + + #region Load Methods + + /// + /// Load a ModelInfo instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ModelInfo instance. + public static ModelInfo Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ModelInfo(); + + + if (data.TryGetValue("id", out var idValue) && idValue is not null) + { + instance.Id = idValue?.ToString()!; + } + + if (data.TryGetValue("displayName", out var displayNameValue) && displayNameValue is not null) + { + instance.DisplayName = displayNameValue?.ToString()!; + } + + if (data.TryGetValue("ownedBy", out var ownedByValue) && ownedByValue is not null) + { + instance.OwnedBy = ownedByValue?.ToString()!; + } + + if (data.TryGetValue("contextWindow", out var contextWindowValue) && contextWindowValue is not null) + { + instance.ContextWindow = Convert.ToInt32(contextWindowValue); + } + + if (data.TryGetValue("inputModalities", out var inputModalitiesValue) && inputModalitiesValue is not null) + { + instance.InputModalities = (inputModalitiesValue as IEnumerable)?.Select(x => x?.ToString()!).ToList() ?? []; + } + + if (data.TryGetValue("outputModalities", out var outputModalitiesValue) && outputModalitiesValue is not null) + { + instance.OutputModalities = (outputModalitiesValue as IEnumerable)?.Select(x => x?.ToString()!).ToList() ?? []; + } + + if (data.TryGetValue("additionalProperties", out var additionalPropertiesValue) && additionalPropertiesValue is not null) + { + instance.AdditionalProperties = additionalPropertiesValue.GetDictionary()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ModelInfo instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["id"] = obj.Id; + + + if (obj.DisplayName is not null) + { + result["displayName"] = obj.DisplayName; + } + + + if (obj.OwnedBy is not null) + { + result["ownedBy"] = obj.OwnedBy; + } + + + if (obj.ContextWindow is not null) + { + result["contextWindow"] = obj.ContextWindow; + } + + + if (obj.InputModalities is not null) + { + result["inputModalities"] = obj.InputModalities; + } + + + if (obj.OutputModalities is not null) + { + result["outputModalities"] = obj.OutputModalities; + } + + + if (obj.AdditionalProperties is not null) + { + result["additionalProperties"] = obj.AdditionalProperties; + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ModelInfo instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ModelInfo instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ModelInfo instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ModelInfo instance. + public static ModelInfo FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ModelInfo instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ModelInfo instance. + public static ModelInfo FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ModelOptions.cs b/runtime/csharp/Prompty.Core/Model/ModelOptions.cs index c22159c9..a2a9e173 100644 --- a/runtime/csharp/Prompty.Core/Model/ModelOptions.cs +++ b/runtime/csharp/Prompty.Core/Model/ModelOptions.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Options for configuring the behavior of the AI model. /// -public class ModelOptions +public partial class ModelOptions { /// /// The shorthand property name for this type, if any. @@ -76,6 +76,7 @@ public ModelOptions() public IDictionary? AdditionalProperties { get; set; } + #region Load Methods /// @@ -154,7 +155,6 @@ public static ModelOptions Load(Dictionary data, LoadContext? c } - #endregion #region Save Methods @@ -181,46 +181,55 @@ public static ModelOptions Load(Dictionary data, LoadContext? c result["frequencyPenalty"] = obj.FrequencyPenalty; } + if (obj.MaxOutputTokens is not null) { result["maxOutputTokens"] = obj.MaxOutputTokens; } + if (obj.PresencePenalty is not null) { result["presencePenalty"] = obj.PresencePenalty; } + if (obj.Seed is not null) { result["seed"] = obj.Seed; } + if (obj.Temperature is not null) { result["temperature"] = obj.Temperature; } + if (obj.TopK is not null) { result["topK"] = obj.TopK; } + if (obj.TopP is not null) { result["topP"] = obj.TopP; } + if (obj.StopSequences is not null) { result["stopSequences"] = obj.StopSequences; } + if (obj.AllowMultipleToolCalls is not null) { result["allowMultipleToolCalls"] = obj.AllowMultipleToolCalls; } + if (obj.AdditionalProperties is not null) { result["additionalProperties"] = obj.AdditionalProperties; @@ -235,6 +244,35 @@ public static ModelOptions Load(Dictionary data, LoadContext? c return result; } + /// + /// Convert this instance to a provider-specific wire-format dictionary. + /// + /// The provider name (e.g., "openai", "anthropic"). + /// A dictionary with provider-specific field names. + public Dictionary ToWire(string provider) + { + var data = Save(); + var result = new Dictionary(); + var wireMap = new Dictionary> + { + ["frequencyPenalty"] = new Dictionary { ["openai"] = "frequency_penalty" }, + ["maxOutputTokens"] = new Dictionary { ["openai"] = "max_completion_tokens", ["responses"] = "max_output_tokens", ["anthropic"] = "max_tokens" }, + ["presencePenalty"] = new Dictionary { ["openai"] = "presence_penalty" }, + ["seed"] = new Dictionary { ["openai"] = "seed" }, + ["temperature"] = new Dictionary { ["openai"] = "temperature", ["responses"] = "temperature", ["anthropic"] = "temperature" }, + ["topK"] = new Dictionary { ["openai"] = "top_k", ["anthropic"] = "top_k" }, + ["topP"] = new Dictionary { ["openai"] = "top_p", ["responses"] = "top_p", ["anthropic"] = "top_p" }, + ["stopSequences"] = new Dictionary { ["openai"] = "stop", ["anthropic"] = "stop_sequences" }, + ["allowMultipleToolCalls"] = new Dictionary { ["openai"] = "parallel_tool_calls" }, + }; + foreach (var (key, value) in data) + { + if (wireMap.TryGetValue(key, out var mapping) && mapping.TryGetValue(provider, out var wireName)) + result[wireName] = value; + } + return result; + } + /// /// Convert the ModelOptions instance to a YAML string. diff --git a/runtime/csharp/Prompty.Core/Model/OAuthConnection.cs b/runtime/csharp/Prompty.Core/Model/OAuthConnection.cs index 6b1a94ed..bf557755 100644 --- a/runtime/csharp/Prompty.Core/Model/OAuthConnection.cs +++ b/runtime/csharp/Prompty.Core/Model/OAuthConnection.cs @@ -8,10 +8,12 @@ namespace Prompty.Core; /// /// Connection configuration using OAuth 2.0 client credentials. +/// /// Useful for tools and services that require OAuth authentication, +/// /// such as MCP servers, OpenAPI endpoints, or other REST APIs. /// -public class OAuthConnection : Connection +public partial class OAuthConnection : Connection { /// /// The shorthand property name for this type, if any. @@ -58,6 +60,7 @@ public OAuthConnection() public IList? Scopes { get; set; } + #region Load Methods /// @@ -116,7 +119,6 @@ public OAuthConnection() } - #endregion #region Save Methods @@ -139,30 +141,20 @@ public OAuthConnection() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Endpoint is not null) - { - result["endpoint"] = obj.Endpoint; - } - if (obj.ClientId is not null) - { - result["clientId"] = obj.ClientId; - } + result["endpoint"] = obj.Endpoint; - if (obj.ClientSecret is not null) - { - result["clientSecret"] = obj.ClientSecret; - } - if (obj.TokenUrl is not null) - { - result["tokenUrl"] = obj.TokenUrl; - } + result["clientId"] = obj.ClientId; + + + result["clientSecret"] = obj.ClientSecret; + + + result["tokenUrl"] = obj.TokenUrl; + if (obj.Scopes is not null) { diff --git a/runtime/csharp/Prompty.Core/Model/ObjectProperty.cs b/runtime/csharp/Prompty.Core/Model/ObjectProperty.cs index 4bb6ee0d..1ef8f5cf 100644 --- a/runtime/csharp/Prompty.Core/Model/ObjectProperty.cs +++ b/runtime/csharp/Prompty.Core/Model/ObjectProperty.cs @@ -8,9 +8,10 @@ namespace Prompty.Core; /// /// Represents an object property. +/// /// This extends the base Property model to represent a structured object. /// -public class ObjectProperty : Property +public partial class ObjectProperty : Property { /// /// The shorthand property name for this type, if any. @@ -27,7 +28,7 @@ public ObjectProperty() #pragma warning restore CS8618 /// - /// + /// Kind /// public override string Kind { get; set; } = "object"; @@ -37,6 +38,7 @@ public ObjectProperty() public IList Properties { get; set; } = []; + #region Load Methods /// @@ -129,7 +131,6 @@ public static IList LoadProperties(object data, LoadContext? context) } - #endregion #region Save Methods @@ -152,15 +153,10 @@ public static IList LoadProperties(object data, LoadContext? context) var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Properties is not null) - { - result["properties"] = SaveProperties(obj.Properties, context); - } + + result["properties"] = SaveProperties(obj.Properties, context); return result; diff --git a/runtime/csharp/Prompty.Core/Model/OpenApiTool.cs b/runtime/csharp/Prompty.Core/Model/OpenApiTool.cs index f1f8d0da..b895d92b 100644 --- a/runtime/csharp/Prompty.Core/Model/OpenApiTool.cs +++ b/runtime/csharp/Prompty.Core/Model/OpenApiTool.cs @@ -7,9 +7,8 @@ namespace Prompty.Core; #pragma warning restore IDE0130 /// -/// /// -public class OpenApiTool : Tool +public partial class OpenApiTool : Tool { /// /// The shorthand property name for this type, if any. @@ -41,6 +40,7 @@ public OpenApiTool() public string Specification { get; set; } = string.Empty; + #region Load Methods /// @@ -84,7 +84,6 @@ public OpenApiTool() } - #endregion #region Save Methods @@ -107,20 +106,13 @@ public OpenApiTool() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Connection is not null) - { - result["connection"] = obj.Connection?.Save(context); - } - if (obj.Specification is not null) - { - result["specification"] = obj.Specification; - } + result["connection"] = obj.Connection?.Save(context); + + + result["specification"] = obj.Specification; return result; diff --git a/runtime/csharp/Prompty.Core/Model/Parser.cs b/runtime/csharp/Prompty.Core/Model/Parser.cs new file mode 100644 index 00000000..c70c01fb --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/Parser.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Parses rendered prompt text into an array of structured messages with role markers. +/// +public interface IParser +{ + /// + /// Pre-process a template before rendering, returning modified template and context + /// + object? PreRender(string template) => default; + /// + /// Parse rendered text into a structured message array + /// + Task> ParseAsync(Prompty agent, string rendered, Dictionary? context); +} diff --git a/runtime/csharp/Prompty.Core/Model/ParserConfig.cs b/runtime/csharp/Prompty.Core/Model/ParserConfig.cs index 78854d8c..04616f9d 100644 --- a/runtime/csharp/Prompty.Core/Model/ParserConfig.cs +++ b/runtime/csharp/Prompty.Core/Model/ParserConfig.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Template parser definition /// -public class ParserConfig +public partial class ParserConfig { /// /// The shorthand property name for this type, if any. @@ -36,6 +36,7 @@ public ParserConfig() public IDictionary? Options { get; set; } + #region Load Methods /// @@ -53,7 +54,6 @@ public static ParserConfig Load(Dictionary data, LoadContext? c // Note: Alternate (shorthand) representations are handled by the converter - // Create new instance var instance = new ParserConfig(); @@ -76,7 +76,6 @@ public static ParserConfig Load(Dictionary data, LoadContext? c } - #endregion #region Save Methods @@ -98,10 +97,8 @@ public static ParserConfig Load(Dictionary data, LoadContext? c var result = new Dictionary(); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + if (obj.Options is not null) { diff --git a/runtime/csharp/Prompty.Core/Model/Processor.cs b/runtime/csharp/Prompty.Core/Model/Processor.cs new file mode 100644 index 00000000..c9d42f2d --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/Processor.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Extracts a clean, typed result from a raw LLM provider response. +/// +public interface IProcessor +{ + /// + /// Extract a clean result from a raw LLM response + /// + Task ProcessAsync(Prompty agent, object response); + /// + /// Process a streaming response into a stream of StreamChunk items. Takes raw chunks from the executor and yields processed text, thinking, tool, or error chunks. Not all providers support streaming; the default implementation should signal lack of support. + /// + Task ProcessStreamAsync(object stream) => Task.FromResult(default); +} diff --git a/runtime/csharp/Prompty.Core/Model/Prompty.cs b/runtime/csharp/Prompty.Core/Model/Prompty.cs index c0ded05e..e5e2a6d3 100644 --- a/runtime/csharp/Prompty.Core/Model/Prompty.cs +++ b/runtime/csharp/Prompty.Core/Model/Prompty.cs @@ -8,13 +8,16 @@ namespace Prompty.Core; /// /// A Prompty is a markdown file format for LLM prompts. The frontmatter defines +/// /// structured metadata including model configuration, input/output schemas, tools, +/// /// and template settings. The markdown body becomes the instructions. /// /// This is the single root type for the Prompty schema — there is no abstract base +/// /// class or kind discriminator. A .prompty file always produces a Prompty instance. /// -public class Prompty +public partial class Prompty { /// /// The shorthand property name for this type, if any. @@ -81,6 +84,7 @@ public Prompty() public string? Instructions { get; set; } + #region Load Methods /// @@ -299,7 +303,7 @@ public static IList LoadTools(object data, LoadContext? context) var newDict = new Dictionary { ["name"] = kvp.Key, - [""] = kvp.Value + ["kind"] = kvp.Value }; result.Add(Tool.Load(newDict, context)); } @@ -321,7 +325,6 @@ public static IList LoadTools(object data, LoadContext? context) } - #endregion #region Save Methods @@ -343,51 +346,54 @@ public static IList LoadTools(object data, LoadContext? context) var result = new Dictionary(); - if (obj.Name is not null) - { - result["name"] = obj.Name; - } + result["name"] = obj.Name; + if (obj.DisplayName is not null) { result["displayName"] = obj.DisplayName; } + if (obj.Description is not null) { result["description"] = obj.Description; } + if (obj.Metadata is not null) { result["metadata"] = obj.Metadata; } + if (obj.Inputs is not null) { result["inputs"] = SaveInputs(obj.Inputs, context); } + if (obj.Outputs is not null) { result["outputs"] = SaveOutputs(obj.Outputs, context); } - if (obj.Model is not null) - { - result["model"] = obj.Model?.Save(context); - } + + result["model"] = obj.Model?.Save(context); + if (obj.Tools is not null) { result["tools"] = SaveTools(obj.Tools, context); } + if (obj.Template is not null) { result["template"] = obj.Template?.Save(context); } + if (obj.Instructions is not null) { result["instructions"] = obj.Instructions; @@ -454,39 +460,8 @@ public static object SaveOutputs(IList items, SaveContext? context) { context ??= new SaveContext(); - - if (context.CollectionFormat == "array") - { - return items.Select(item => item.Save(context)).ToList(); - } - - // Object format: use name as key - var result = new Dictionary(); - foreach (var item in items) - { - var itemData = item.Save(context); - if (itemData.TryGetValue("name", out var nameValue) && nameValue is string name) - { - itemData.Remove("name"); - - // Check if we can use shorthand - if (context.UseShorthand && Property.ShorthandProperty is string shorthandProp) - { - if (itemData.Count == 1 && itemData.ContainsKey(shorthandProp)) - { - result[name] = itemData[shorthandProp]; - continue; - } - } - result[name] = itemData; - } - else - { - // No name, can't use object format for this item - throw new InvalidOperationException("Cannot save item in object format: missing 'name' property"); - } - } - return result; + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); } diff --git a/runtime/csharp/Prompty.Core/Model/PromptyTool.cs b/runtime/csharp/Prompty.Core/Model/PromptyTool.cs index 7307bada..0c828f2b 100644 --- a/runtime/csharp/Prompty.Core/Model/PromptyTool.cs +++ b/runtime/csharp/Prompty.Core/Model/PromptyTool.cs @@ -10,9 +10,10 @@ namespace Prompty.Core; /// A tool that references another .prompty file to be invoked as a tool. /// /// In `single` mode, the child prompty is executed with a single LLM call. +/// /// In `agentic` mode, the child prompty runs a full agent loop with its own tools. /// -public class PromptyTool : Tool +public partial class PromptyTool : Tool { /// /// The shorthand property name for this type, if any. @@ -44,6 +45,7 @@ public PromptyTool() public string Mode { get; set; } = "single"; + #region Load Methods /// @@ -87,7 +89,6 @@ public PromptyTool() } - #endregion #region Save Methods @@ -110,20 +111,13 @@ public PromptyTool() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Path is not null) - { - result["path"] = obj.Path; - } - if (obj.Mode is not null) - { - result["mode"] = obj.Mode; - } + result["path"] = obj.Path; + + + result["mode"] = obj.Mode; return result; diff --git a/runtime/csharp/Prompty.Core/Model/Property.cs b/runtime/csharp/Prompty.Core/Model/Property.cs index 12b87238..38ac93a6 100644 --- a/runtime/csharp/Prompty.Core/Model/Property.cs +++ b/runtime/csharp/Prompty.Core/Model/Property.cs @@ -10,11 +10,14 @@ namespace Prompty.Core; /// Represents a single property. /// /// - This model defines the structure of properties that can be used in prompts, +/// /// including their type, description, whether they are required, and other attributes. +/// /// - It allows for the definition of dynamic inputs that can be filled with data +/// /// and processed to generate prompts for AI models. /// -public class Property +public partial class Property { /// /// The shorthand property name for this type, if any. @@ -66,6 +69,7 @@ public Property() public IList? EnumValues { get; set; } + #region Load Methods /// @@ -83,7 +87,6 @@ public static Property Load(Dictionary data, LoadContext? conte // Note: Alternate (shorthand) representations are handled by the converter - // Load polymorphic Property instance var instance = LoadKind(data, context); @@ -131,7 +134,6 @@ public static Property Load(Dictionary data, LoadContext? conte } - /// /// Load polymorphic Property based on discriminator. /// @@ -174,36 +176,36 @@ private static Property LoadKind(Dictionary data, LoadContext? var result = new Dictionary(); - if (obj.Name is not null) - { - result["name"] = obj.Name; - } + result["name"] = obj.Name; + + + result["kind"] = obj.Kind; - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } if (obj.Description is not null) { result["description"] = obj.Description; } + if (obj.Required is not null) { result["required"] = obj.Required; } + if (obj.Default is not null) { result["default"] = obj.Default; } + if (obj.Example is not null) { result["example"] = obj.Example; } + if (obj.EnumValues is not null) { result["enumValues"] = obj.EnumValues; @@ -263,16 +265,16 @@ public static Property FromJson(string json, LoadContext? context = null) ["kind"] = "boolean", ["example"] = boolValue, }, - string stringValue => new Dictionary - { - ["kind"] = "string", - ["example"] = stringValue, - }, float floatValue => new Dictionary { ["kind"] = "float", ["example"] = floatValue, }, + double doubleValue => new Dictionary + { + ["kind"] = "float", + ["example"] = doubleValue, + }, int intValue => new Dictionary { ["kind"] = "integer", @@ -283,10 +285,10 @@ public static Property FromJson(string json, LoadContext? context = null) ["kind"] = "integer", ["example"] = longValue, }, - double doubleValue => new Dictionary + string stringValue => new Dictionary { - ["kind"] = "float", - ["example"] = doubleValue, + ["kind"] = "string", + ["example"] = stringValue, }, _ => new Dictionary { @@ -338,16 +340,16 @@ public static Property FromYaml(string yaml, LoadContext? context = null) ["kind"] = "boolean", ["example"] = boolValue, }, - string stringValue => new Dictionary - { - ["kind"] = "string", - ["example"] = stringValue, - }, float floatValue => new Dictionary { ["kind"] = "float", ["example"] = floatValue, }, + double doubleValue => new Dictionary + { + ["kind"] = "float", + ["example"] = doubleValue, + }, int intValue => new Dictionary { ["kind"] = "integer", @@ -358,10 +360,10 @@ public static Property FromYaml(string yaml, LoadContext? context = null) ["kind"] = "integer", ["example"] = longValue, }, - double doubleValue => new Dictionary + string stringValue => new Dictionary { - ["kind"] = "float", - ["example"] = doubleValue, + ["kind"] = "string", + ["example"] = stringValue, }, _ => new Dictionary { diff --git a/runtime/csharp/Prompty.Core/Model/ReferenceConnection.cs b/runtime/csharp/Prompty.Core/Model/ReferenceConnection.cs index eedd0d30..6df6fe5a 100644 --- a/runtime/csharp/Prompty.Core/Model/ReferenceConnection.cs +++ b/runtime/csharp/Prompty.Core/Model/ReferenceConnection.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Connection configuration for AI services using named connections. /// -public class ReferenceConnection : Connection +public partial class ReferenceConnection : Connection { /// /// The shorthand property name for this type, if any. @@ -41,6 +41,7 @@ public ReferenceConnection() public string? Target { get; set; } + #region Load Methods /// @@ -84,7 +85,6 @@ public ReferenceConnection() } - #endregion #region Save Methods @@ -107,15 +107,11 @@ public ReferenceConnection() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; + + + result["name"] = obj.Name; - if (obj.Name is not null) - { - result["name"] = obj.Name; - } if (obj.Target is not null) { diff --git a/runtime/csharp/Prompty.Core/Model/RemoteConnection.cs b/runtime/csharp/Prompty.Core/Model/RemoteConnection.cs index 90c6e617..d94c4763 100644 --- a/runtime/csharp/Prompty.Core/Model/RemoteConnection.cs +++ b/runtime/csharp/Prompty.Core/Model/RemoteConnection.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Connection configuration for AI services using named connections. /// -public class RemoteConnection : Connection +public partial class RemoteConnection : Connection { /// /// The shorthand property name for this type, if any. @@ -41,6 +41,7 @@ public RemoteConnection() public string Endpoint { get; set; } = string.Empty; + #region Load Methods /// @@ -84,7 +85,6 @@ public RemoteConnection() } - #endregion #region Save Methods @@ -107,20 +107,13 @@ public RemoteConnection() var result = base.Save(context); - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } + result["kind"] = obj.Kind; - if (obj.Name is not null) - { - result["name"] = obj.Name; - } - if (obj.Endpoint is not null) - { - result["endpoint"] = obj.Endpoint; - } + result["name"] = obj.Name; + + + result["endpoint"] = obj.Endpoint; return result; diff --git a/runtime/csharp/Prompty.Core/Model/Renderer.cs b/runtime/csharp/Prompty.Core/Model/Renderer.cs new file mode 100644 index 00000000..67682679 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/Renderer.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Renders a template string with input values to produce the final prompt text. +/// +public interface IRenderer +{ + /// + /// Render the template string with input values + /// + Task RenderAsync(Prompty agent, string template, Dictionary inputs); +} diff --git a/runtime/csharp/Prompty.Core/Model/StatusEventPayload.cs b/runtime/csharp/Prompty.Core/Model/StatusEventPayload.cs new file mode 100644 index 00000000..a1372eaf --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/StatusEventPayload.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "status" events — informational messages about loop progress. +/// +public partial class StatusEventPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public StatusEventPayload() + { + } +#pragma warning restore CS8618 + + /// + /// Human-readable status message + /// + public string Message { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a StatusEventPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded StatusEventPayload instance. + public static StatusEventPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new StatusEventPayload(); + + + if (data.TryGetValue("message", out var messageValue) && messageValue is not null) + { + instance.Message = messageValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the StatusEventPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["message"] = obj.Message; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the StatusEventPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the StatusEventPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a StatusEventPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded StatusEventPayload instance. + public static StatusEventPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a StatusEventPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded StatusEventPayload instance. + public static StatusEventPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/StreamChunk.cs b/runtime/csharp/Prompty.Core/Model/StreamChunk.cs new file mode 100644 index 00000000..2fcc0b9d --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/StreamChunk.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A chunk of data from a streaming LLM response. Stream chunks are +/// +/// discriminated on the `kind` field. +/// +public abstract partial class StreamChunk +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + protected StreamChunk() + { + } +#pragma warning restore CS8618 + + /// + /// The kind of stream chunk + /// + public virtual string Kind { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a StreamChunk instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded StreamChunk instance. + public static StreamChunk Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Load polymorphic StreamChunk instance + var instance = LoadKind(data, context); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load polymorphic StreamChunk based on discriminator. + /// + private static StreamChunk LoadKind(Dictionary data, LoadContext? context) + { + if (data.TryGetValue("kind", out var discriminatorValue) && discriminatorValue is not null) + { + var discriminator = discriminatorValue.ToString()?.ToLowerInvariant(); + return discriminator switch + { + "text" => TextChunk.Load(data, context), + "thinking" => ThinkingChunk.Load(data, context), + "tool" => ToolChunk.Load(data, context), + "error" => ErrorChunk.Load(data, context), + _ => throw new ArgumentException($"Unknown StreamChunk discriminator value: {discriminator}"), + }; + } + + throw new ArgumentException("Missing StreamChunk discriminator property: 'kind'"); + + } + + + #endregion + + #region Save Methods + + /// + /// Save the StreamChunk instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public virtual Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["kind"] = obj.Kind; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the StreamChunk instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the StreamChunk instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a StreamChunk instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded StreamChunk instance. + public static StreamChunk FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a StreamChunk instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded StreamChunk instance. + public static StreamChunk FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/Template.cs b/runtime/csharp/Prompty.Core/Model/Template.cs index c356d8c5..2484a19c 100644 --- a/runtime/csharp/Prompty.Core/Model/Template.cs +++ b/runtime/csharp/Prompty.Core/Model/Template.cs @@ -10,13 +10,16 @@ namespace Prompty.Core; /// Template model for defining prompt templates. /// /// This model specifies the rendering engine used for slot filling prompts, +/// /// the parser used to process the rendered template into API-compatible format, +/// /// and additional options for the template engine. /// /// It allows for the creation of reusable templates that can be filled with dynamic data +/// /// and processed to generate prompts for AI models. /// -public class Template +public partial class Template { /// /// The shorthand property name for this type, if any. @@ -43,6 +46,7 @@ public Template() public ParserConfig Parser { get; set; } + #region Load Methods /// @@ -81,7 +85,6 @@ public static Template Load(Dictionary data, LoadContext? conte } - #endregion #region Save Methods @@ -103,15 +106,10 @@ public static Template Load(Dictionary data, LoadContext? conte var result = new Dictionary(); - if (obj.Format is not null) - { - result["format"] = obj.Format?.Save(context); - } + result["format"] = obj.Format?.Save(context); - if (obj.Parser is not null) - { - result["parser"] = obj.Parser?.Save(context); - } + + result["parser"] = obj.Parser?.Save(context); if (context is not null) diff --git a/runtime/csharp/Prompty.Core/Model/TextChunk.cs b/runtime/csharp/Prompty.Core/Model/TextChunk.cs new file mode 100644 index 00000000..12cce412 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/TextChunk.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A text content chunk from the LLM response stream. +/// +public partial class TextChunk : StreamChunk +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public TextChunk() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for text chunks + /// + public override string Kind { get; set; } = "text"; + + /// + /// The text content of the chunk + /// + public string Value { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a TextChunk instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded TextChunk instance. + public new static TextChunk Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new TextChunk(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("value", out var valueValue) && valueValue is not null) + { + instance.Value = valueValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the TextChunk instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["value"] = obj.Value; + + + return result; + } + + + /// + /// Convert the TextChunk instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the TextChunk instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a TextChunk instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TextChunk instance. + public new static TextChunk FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a TextChunk instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TextChunk instance. + public new static TextChunk FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/TextPart.cs b/runtime/csharp/Prompty.Core/Model/TextPart.cs new file mode 100644 index 00000000..ef0d1dc3 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/TextPart.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A text content part. +/// +public partial class TextPart : ContentPart +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public TextPart() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for text content + /// + public override string Kind { get; set; } = "text"; + + /// + /// The text content + /// + public string Value { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a TextPart instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded TextPart instance. + public new static TextPart Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new TextPart(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("value", out var valueValue) && valueValue is not null) + { + instance.Value = valueValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the TextPart instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["value"] = obj.Value; + + + return result; + } + + + /// + /// Convert the TextPart instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the TextPart instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a TextPart instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TextPart instance. + public new static TextPart FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a TextPart instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TextPart instance. + public new static TextPart FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ThinkingChunk.cs b/runtime/csharp/Prompty.Core/Model/ThinkingChunk.cs new file mode 100644 index 00000000..a75d4497 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ThinkingChunk.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A thinking/reasoning content chunk from the LLM response stream. +/// +public partial class ThinkingChunk : StreamChunk +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ThinkingChunk() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for thinking chunks + /// + public override string Kind { get; set; } = "thinking"; + + /// + /// The thinking content of the chunk + /// + public string Value { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ThinkingChunk instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ThinkingChunk instance. + public new static ThinkingChunk Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ThinkingChunk(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("value", out var valueValue) && valueValue is not null) + { + instance.Value = valueValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ThinkingChunk instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["value"] = obj.Value; + + + return result; + } + + + /// + /// Convert the ThinkingChunk instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ThinkingChunk instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ThinkingChunk instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ThinkingChunk instance. + public new static ThinkingChunk FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ThinkingChunk instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ThinkingChunk instance. + public new static ThinkingChunk FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ThinkingEventPayload.cs b/runtime/csharp/Prompty.Core/Model/ThinkingEventPayload.cs new file mode 100644 index 00000000..15ec5cbc --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ThinkingEventPayload.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "thinking" events — reasoning/chain-of-thought tokens. +/// +public partial class ThinkingEventPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ThinkingEventPayload() + { + } +#pragma warning restore CS8618 + + /// + /// The thinking/reasoning token text + /// + public string Token { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ThinkingEventPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ThinkingEventPayload instance. + public static ThinkingEventPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ThinkingEventPayload(); + + + if (data.TryGetValue("token", out var tokenValue) && tokenValue is not null) + { + instance.Token = tokenValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ThinkingEventPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["token"] = obj.Token; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ThinkingEventPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ThinkingEventPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ThinkingEventPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ThinkingEventPayload instance. + public static ThinkingEventPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ThinkingEventPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ThinkingEventPayload instance. + public static ThinkingEventPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ThreadMarker.cs b/runtime/csharp/Prompty.Core/Model/ThreadMarker.cs new file mode 100644 index 00000000..7cf74f85 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ThreadMarker.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Positional marker for conversation history insertion during template rendering. +/// +/// During `prepare()`, nonce strings in rendered text are replaced with +/// +/// ThreadMarker objects. The pipeline then replaces them with actual +/// +/// conversation messages from the inputs. +/// +public partial class ThreadMarker +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ThreadMarker() + { + } +#pragma warning restore CS8618 + + /// + /// The input property name (e.g. 'conversation') + /// + public string Name { get; set; } = "thread"; + + /// + /// The rich kind ('thread', 'image', 'file', 'audio') + /// + public string Kind { get; set; } = "thread"; + + + + #region Load Methods + + /// + /// Load a ThreadMarker instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ThreadMarker instance. + public static ThreadMarker Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ThreadMarker(); + + + if (data.TryGetValue("name", out var nameValue) && nameValue is not null) + { + instance.Name = nameValue?.ToString()!; + } + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ThreadMarker instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["name"] = obj.Name; + + + result["kind"] = obj.Kind; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ThreadMarker instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ThreadMarker instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ThreadMarker instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ThreadMarker instance. + public static ThreadMarker FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ThreadMarker instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ThreadMarker instance. + public static ThreadMarker FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/TokenEventPayload.cs b/runtime/csharp/Prompty.Core/Model/TokenEventPayload.cs new file mode 100644 index 00000000..46d720e0 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/TokenEventPayload.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "token" events — a single text token streamed from the LLM. +/// +public partial class TokenEventPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public TokenEventPayload() + { + } +#pragma warning restore CS8618 + + /// + /// The streamed token text + /// + public string Token { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a TokenEventPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded TokenEventPayload instance. + public static TokenEventPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new TokenEventPayload(); + + + if (data.TryGetValue("token", out var tokenValue) && tokenValue is not null) + { + instance.Token = tokenValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the TokenEventPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["token"] = obj.Token; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the TokenEventPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the TokenEventPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a TokenEventPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TokenEventPayload instance. + public static TokenEventPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a TokenEventPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TokenEventPayload instance. + public static TokenEventPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/TokenUsage.cs b/runtime/csharp/Prompty.Core/Model/TokenUsage.cs new file mode 100644 index 00000000..ace9cff2 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/TokenUsage.cs @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Tracks token consumption for a single LLM call. Provider-specific field +/// +/// names (e.g., OpenAI's `prompt_tokens` vs Anthropic's `input_tokens`) +/// +/// are mapped via `knownAs` augments in the wire directory. +/// +public partial class TokenUsage +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public TokenUsage() + { + } +#pragma warning restore CS8618 + + /// + /// Number of tokens in the prompt/input sent to the model + /// + public int? PromptTokens { get; set; } + + /// + /// Number of tokens generated in the model's completion/output + /// + public int? CompletionTokens { get; set; } + + /// + /// Total tokens consumed (prompt + completion). May be provided by the API or computed. + /// + public int? TotalTokens { get; set; } + + + + #region Load Methods + + /// + /// Load a TokenUsage instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded TokenUsage instance. + public static TokenUsage Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new TokenUsage(); + + + if (data.TryGetValue("promptTokens", out var promptTokensValue) && promptTokensValue is not null) + { + instance.PromptTokens = Convert.ToInt32(promptTokensValue); + } + + if (data.TryGetValue("completionTokens", out var completionTokensValue) && completionTokensValue is not null) + { + instance.CompletionTokens = Convert.ToInt32(completionTokensValue); + } + + if (data.TryGetValue("totalTokens", out var totalTokensValue) && totalTokensValue is not null) + { + instance.TotalTokens = Convert.ToInt32(totalTokensValue); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the TokenUsage instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + if (obj.PromptTokens is not null) + { + result["promptTokens"] = obj.PromptTokens; + } + + + if (obj.CompletionTokens is not null) + { + result["completionTokens"] = obj.CompletionTokens; + } + + + if (obj.TotalTokens is not null) + { + result["totalTokens"] = obj.TotalTokens; + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + /// + /// Convert this instance to a provider-specific wire-format dictionary. + /// + /// The provider name (e.g., "openai", "anthropic"). + /// A dictionary with provider-specific field names. + public Dictionary ToWire(string provider) + { + var data = Save(); + var result = new Dictionary(); + var wireMap = new Dictionary> + { + ["promptTokens"] = new Dictionary { ["openai"] = "prompt_tokens", ["anthropic"] = "input_tokens" }, + ["completionTokens"] = new Dictionary { ["openai"] = "completion_tokens", ["anthropic"] = "output_tokens" }, + ["totalTokens"] = new Dictionary { ["openai"] = "total_tokens" }, + }; + foreach (var (key, value) in data) + { + if (wireMap.TryGetValue(key, out var mapping) && mapping.TryGetValue(provider, out var wireName)) + result[wireName] = value; + } + return result; + } + + + /// + /// Convert the TokenUsage instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the TokenUsage instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a TokenUsage instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TokenUsage instance. + public static TokenUsage FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a TokenUsage instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TokenUsage instance. + public static TokenUsage FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/Tool.cs b/runtime/csharp/Prompty.Core/Model/Tool.cs index 6914f08b..3a19125c 100644 --- a/runtime/csharp/Prompty.Core/Model/Tool.cs +++ b/runtime/csharp/Prompty.Core/Model/Tool.cs @@ -9,7 +9,7 @@ namespace Prompty.Core; /// /// Represents a tool that can be used in prompts. /// -public abstract class Tool +public abstract partial class Tool { /// /// The shorthand property name for this type, if any. @@ -46,6 +46,7 @@ protected Tool() public IList? Bindings { get; set; } + #region Load Methods /// @@ -148,7 +149,6 @@ public static IList LoadBindings(object data, LoadContext? context) } - /// /// Load polymorphic Tool based on discriminator. /// @@ -193,21 +193,18 @@ private static Tool LoadKind(Dictionary data, LoadContext? cont var result = new Dictionary(); - if (obj.Name is not null) - { - result["name"] = obj.Name; - } + result["name"] = obj.Name; + + + result["kind"] = obj.Kind; - if (obj.Kind is not null) - { - result["kind"] = obj.Kind; - } if (obj.Description is not null) { result["description"] = obj.Description; } + if (obj.Bindings is not null) { result["bindings"] = SaveBindings(obj.Bindings, context); diff --git a/runtime/csharp/Prompty.Core/Model/ToolCall.cs b/runtime/csharp/Prompty.Core/Model/ToolCall.cs new file mode 100644 index 00000000..4c68a00e --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolCall.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A tool call requested by the LLM. Contains the function name and serialized +/// +/// arguments that should be dispatched to the appropriate tool handler. +/// +public partial class ToolCall +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolCall() + { + } +#pragma warning restore CS8618 + + /// + /// The unique identifier of the tool call + /// + public string Id { get; set; } = string.Empty; + + /// + /// The name of the tool to call + /// + public string Name { get; set; } = string.Empty; + + /// + /// The serialized JSON arguments for the tool call + /// + public string Arguments { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ToolCall instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolCall instance. + public static ToolCall Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolCall(); + + + if (data.TryGetValue("id", out var idValue) && idValue is not null) + { + instance.Id = idValue?.ToString()!; + } + + if (data.TryGetValue("name", out var nameValue) && nameValue is not null) + { + instance.Name = nameValue?.ToString()!; + } + + if (data.TryGetValue("arguments", out var argumentsValue) && argumentsValue is not null) + { + instance.Arguments = argumentsValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolCall instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["id"] = obj.Id; + + + result["name"] = obj.Name; + + + result["arguments"] = obj.Arguments; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ToolCall instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolCall instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolCall instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolCall instance. + public static ToolCall FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolCall instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolCall instance. + public static ToolCall FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolCallStartPayload.cs b/runtime/csharp/Prompty.Core/Model/ToolCallStartPayload.cs new file mode 100644 index 00000000..7527106d --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolCallStartPayload.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "tool_call_start" events — the LLM has requested a tool call. +/// +public partial class ToolCallStartPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolCallStartPayload() + { + } +#pragma warning restore CS8618 + + /// + /// The name of the tool being called + /// + public string Name { get; set; } = string.Empty; + + /// + /// The serialized JSON arguments for the tool call + /// + public string Arguments { get; set; } = string.Empty; + + + + #region Load Methods + + /// + /// Load a ToolCallStartPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolCallStartPayload instance. + public static ToolCallStartPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolCallStartPayload(); + + + if (data.TryGetValue("name", out var nameValue) && nameValue is not null) + { + instance.Name = nameValue?.ToString()!; + } + + if (data.TryGetValue("arguments", out var argumentsValue) && argumentsValue is not null) + { + instance.Arguments = argumentsValue?.ToString()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolCallStartPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["name"] = obj.Name; + + + result["arguments"] = obj.Arguments; + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ToolCallStartPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolCallStartPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolCallStartPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolCallStartPayload instance. + public static ToolCallStartPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolCallStartPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolCallStartPayload instance. + public static ToolCallStartPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolChunk.cs b/runtime/csharp/Prompty.Core/Model/ToolChunk.cs new file mode 100644 index 00000000..ec25e197 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolChunk.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// A tool call chunk from the LLM response stream. +/// +public partial class ToolChunk : StreamChunk +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolChunk() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for tool chunks + /// + public override string Kind { get; set; } = "tool"; + + /// + /// The tool call data + /// + public ToolCall ToolCall { get; set; } + + + + #region Load Methods + + /// + /// Load a ToolChunk instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolChunk instance. + public new static ToolChunk Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolChunk(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("toolCall", out var toolCallValue) && toolCallValue is not null) + { + instance.ToolCall = ToolCall.Load(toolCallValue.GetDictionary(ToolCall.ShorthandProperty), context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolChunk instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + result["kind"] = obj.Kind; + + + result["toolCall"] = obj.ToolCall?.Save(context); + + + return result; + } + + + /// + /// Convert the ToolChunk instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolChunk instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolChunk instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolChunk instance. + public new static ToolChunk FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolChunk instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolChunk instance. + public new static ToolChunk FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolContext.cs b/runtime/csharp/Prompty.Core/Model/ToolContext.cs new file mode 100644 index 00000000..7e7e3d72 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolContext.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Context passed to tool handlers during agent loop execution. Provides +/// +/// access to the agent configuration, current conversation state, and +/// +/// arbitrary metadata for tool implementations that need broader context. +/// +public partial class ToolContext +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolContext() + { + } +#pragma warning restore CS8618 + + /// + /// The current conversation messages at the point of tool invocation + /// + public IList Messages { get; set; } = []; + + /// + /// Optional metadata for tool-specific context (e.g., user session info) + /// + public IDictionary? Metadata { get; set; } + + + + #region Load Methods + + /// + /// Load a ToolContext instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolContext instance. + public static ToolContext Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolContext(); + + + if (data.TryGetValue("messages", out var messagesValue) && messagesValue is not null) + { + instance.Messages = LoadMessages(messagesValue, context); + } + + if (data.TryGetValue("metadata", out var metadataValue) && metadataValue is not null) + { + instance.Metadata = metadataValue.GetDictionary()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load a list of Message from a dictionary or list. + /// + public static IList LoadMessages(object data, LoadContext? context) + { + var result = new List(); + + if (data is Dictionary dict) + { + // Convert named dictionary to list + foreach (var kvp in dict) + { + if (kvp.Value is IEnumerable) + { + throw new ArgumentException( + $"Invalid 'messages' format: key '{kvp.Key}' has an array value. " + + $"'messages' must be a flat list of objects or a name-keyed dict — " + + "not a nested {" + kvp.Key + ": [...]} structure."); + } + var itemDict = kvp.Value.GetDictionary(); + if (itemDict.Count > 0) + { + // Value is an object, add name to it + itemDict["name"] = kvp.Key; + result.Add(Message.Load(itemDict, context)); + } + else + { + // Value is a scalar, use it as the primary property + var newDict = new Dictionary + { + ["name"] = kvp.Key, + ["role"] = kvp.Value + }; + result.Add(Message.Load(newDict, context)); + } + } + } + else if (data is IEnumerable list) + { + foreach (var item in list) + { + var itemDict = item.GetDictionary(Message.ShorthandProperty); + if (itemDict.Count > 0) + { + result.Add(Message.Load(itemDict, context)); + } + } + } + + return result; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolContext instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["messages"] = SaveMessages(obj.Messages, context); + + + if (obj.Metadata is not null) + { + result["metadata"] = obj.Metadata; + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Save a list of Message to object or array format. + /// + public static object SaveMessages(IList items, SaveContext? context) + { + context ??= new SaveContext(); + + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); + + } + + + /// + /// Convert the ToolContext instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolContext instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolContext instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolContext instance. + public static ToolContext FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolContext instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolContext instance. + public static ToolContext FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolDispatchResult.cs b/runtime/csharp/Prompty.Core/Model/ToolDispatchResult.cs new file mode 100644 index 00000000..5fa86f43 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolDispatchResult.cs @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// The result of dispatching a single tool call. Pairs the tool call +/// +/// identifier with the tool's name and result for correlation in the +/// +/// agent loop's message assembly. +/// +public partial class ToolDispatchResult +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolDispatchResult() + { + } +#pragma warning restore CS8618 + + /// + /// The tool call ID from the LLM response, used to correlate results + /// + public string ToolCallId { get; set; } = string.Empty; + + /// + /// The name of the tool that was called + /// + public string Name { get; set; } = string.Empty; + + /// + /// The result produced by the tool handler + /// + public ToolResult Result { get; set; } + + + + #region Load Methods + + /// + /// Load a ToolDispatchResult instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolDispatchResult instance. + public static ToolDispatchResult Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolDispatchResult(); + + + if (data.TryGetValue("toolCallId", out var toolCallIdValue) && toolCallIdValue is not null) + { + instance.ToolCallId = toolCallIdValue?.ToString()!; + } + + if (data.TryGetValue("name", out var nameValue) && nameValue is not null) + { + instance.Name = nameValue?.ToString()!; + } + + if (data.TryGetValue("result", out var resultValue) && resultValue is not null) + { + instance.Result = ToolResult.Load(resultValue.GetDictionary(ToolResult.ShorthandProperty), context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolDispatchResult instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["toolCallId"] = obj.ToolCallId; + + + result["name"] = obj.Name; + + + result["result"] = obj.Result?.Save(context); + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ToolDispatchResult instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolDispatchResult instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolDispatchResult instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolDispatchResult instance. + public static ToolDispatchResult FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolDispatchResult instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolDispatchResult instance. + public static ToolDispatchResult FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolResult.cs b/runtime/csharp/Prompty.Core/Model/ToolResult.cs new file mode 100644 index 00000000..9f045d87 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolResult.cs @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// The result of a tool execution. Contains a list of content parts, enabling +/// +/// rich tool results (text, images, files, audio) rather than just strings. +/// +/// Implementations MUST support conversion from a plain string to a ToolResult +/// +/// containing a single TextPart for backward compatibility. +/// +public partial class ToolResult : IToolResultHelpers +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolResult() + { + } +#pragma warning restore CS8618 + + /// + /// The content parts of the tool result + /// + public IList Parts { get; set; } = []; + + + + #region Load Methods + + /// + /// Load a ToolResult instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolResult instance. + public static ToolResult Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolResult(); + + + if (data.TryGetValue("parts", out var partsValue) && partsValue is not null) + { + instance.Parts = LoadParts(partsValue, context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load a list of ContentPart from a dictionary or list. + /// + public static IList LoadParts(object data, LoadContext? context) + { + var result = new List(); + + if (data is Dictionary dict) + { + // Convert named dictionary to list + foreach (var kvp in dict) + { + if (kvp.Value is IEnumerable) + { + throw new ArgumentException( + $"Invalid 'parts' format: key '{kvp.Key}' has an array value. " + + $"'parts' must be a flat list of objects or a name-keyed dict — " + + "not a nested {" + kvp.Key + ": [...]} structure."); + } + var itemDict = kvp.Value.GetDictionary(); + if (itemDict.Count > 0) + { + // Value is an object, add name to it + itemDict["name"] = kvp.Key; + result.Add(ContentPart.Load(itemDict, context)); + } + else + { + // Value is a scalar, use it as the primary property + var newDict = new Dictionary + { + ["name"] = kvp.Key, + ["kind"] = kvp.Value + }; + result.Add(ContentPart.Load(newDict, context)); + } + } + } + else if (data is IEnumerable list) + { + foreach (var item in list) + { + var itemDict = item.GetDictionary(ContentPart.ShorthandProperty); + if (itemDict.Count > 0) + { + result.Add(ContentPart.Load(itemDict, context)); + } + } + } + + return result; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolResult instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["parts"] = SaveParts(obj.Parts, context); + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Save a list of ContentPart to object or array format. + /// + public static object SaveParts(IList items, SaveContext? context) + { + context ??= new SaveContext(); + + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); + + } + + + /// + /// Convert the ToolResult instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolResult instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolResult instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolResult instance. + public static ToolResult FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolResult instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolResult instance. + public static ToolResult FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion + + #region Factory Methods + + /// + /// Create a ToolResult with preset field values. + /// + public static ToolResult CreateText(string value) + { + return new ToolResult { Parts = new List { new TextPart { Value = value } } }; + } + + #endregion +} + +/// +/// Helper contract for . +/// +/// Runtime implementations must provide these members on ToolResult (via a +/// hand-written partial class). The C# compiler enforces conformance +/// because ToolResult declares : IToolResultHelpers. +/// +public partial interface IToolResultHelpers +{ + /// + /// Concatenate all TextPart values joined by newline + /// + string Text { get; } +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolResultHelpers.cs b/runtime/csharp/Prompty.Core/Model/ToolResultHelpers.cs new file mode 100644 index 00000000..aa46ebe4 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolResultHelpers.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +// --- Runtime helpers (manually maintained) --- +// This file extends the generated ToolResult class with convenience members +// used by the Prompty pipeline and tool dispatch system. + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +public partial class ToolResult +{ + /// + /// Create a ToolResult from a plain text string (wraps in a single TextPart). + /// + public static ToolResult FromText(string text) => + new() { Parts = [new TextPart { Value = text }] }; + + /// + /// Concatenated text from all TextParts in this result. + /// + public string Text => string.Join("", Parts.OfType().Select(p => p.Value)); + + /// + /// Implicit conversion from string for backward compatibility. + /// Existing code returning strings from tool handlers will continue to work. + /// + public static implicit operator ToolResult(string text) => FromText(text); +} diff --git a/runtime/csharp/Prompty.Core/Model/ToolResultPayload.cs b/runtime/csharp/Prompty.Core/Model/ToolResultPayload.cs new file mode 100644 index 00000000..07e378be --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/ToolResultPayload.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Payload for "tool_result" events — a tool has returned its result. +/// +public partial class ToolResultPayload +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolResultPayload() + { + } +#pragma warning restore CS8618 + + /// + /// The name of the tool that produced the result + /// + public string Name { get; set; } = string.Empty; + + /// + /// The tool's result + /// + public ToolResult Result { get; set; } + + + + #region Load Methods + + /// + /// Load a ToolResultPayload instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolResultPayload instance. + public static ToolResultPayload Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolResultPayload(); + + + if (data.TryGetValue("name", out var nameValue) && nameValue is not null) + { + instance.Name = nameValue?.ToString()!; + } + + if (data.TryGetValue("result", out var resultValue) && resultValue is not null) + { + instance.Result = ToolResult.Load(resultValue.GetDictionary(ToolResult.ShorthandProperty), context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the ToolResultPayload instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + result["name"] = obj.Name; + + + result["result"] = obj.Result?.Save(context); + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ToolResultPayload instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolResultPayload instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolResultPayload instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolResultPayload instance. + public static ToolResultPayload FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolResultPayload instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolResultPayload instance. + public static ToolResultPayload FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Model/TurnOptions.cs b/runtime/csharp/Prompty.Core/Model/TurnOptions.cs new file mode 100644 index 00000000..99d25995 --- /dev/null +++ b/runtime/csharp/Prompty.Core/Model/TurnOptions.cs @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace Prompty.Core; +#pragma warning restore IDE0130 + +/// +/// Configuration for the agent loop's turn() function. Controls iteration +/// +/// limits, retry policy, context management, and execution behavior. +/// +/// Runtimes accept these as either a TurnOptions object or individual +/// +/// keyword/named parameters — the TypeSpec model defines the canonical +/// +/// field set. +/// +public partial class TurnOptions +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public TurnOptions() + { + } +#pragma warning restore CS8618 + + /// + /// Maximum number of tool-call iterations before the loop terminates + /// + public int? MaxIterations { get; set; } + + /// + /// Maximum number of LLM call retries on transient failure within a single iteration + /// + public int? MaxLlmRetries { get; set; } + + /// + /// Character budget for the context window. When set, the loop trims older messages to stay within budget before each LLM call. System messages are never dropped. + /// + public int? ContextBudget { get; set; } + + /// + /// Whether to execute multiple tool calls concurrently when the LLM returns several in one response + /// + public bool? ParallelToolCalls { get; set; } + + /// + /// When true, return the raw LLM response without processor post-processing + /// + public bool? Raw { get; set; } + + /// + /// Turn number for trace labeling. Useful when the caller maintains an outer conversation loop. + /// + public int? Turn { get; set; } + + /// + /// Context compaction configuration. Controls how messages are summarized when the context window exceeds the budget. + /// + public CompactionConfig? Compaction { get; set; } + + + + #region Load Methods + + /// + /// Load a TurnOptions instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded TurnOptions instance. + public static TurnOptions Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new TurnOptions(); + + + if (data.TryGetValue("maxIterations", out var maxIterationsValue) && maxIterationsValue is not null) + { + instance.MaxIterations = Convert.ToInt32(maxIterationsValue); + } + + if (data.TryGetValue("maxLlmRetries", out var maxLlmRetriesValue) && maxLlmRetriesValue is not null) + { + instance.MaxLlmRetries = Convert.ToInt32(maxLlmRetriesValue); + } + + if (data.TryGetValue("contextBudget", out var contextBudgetValue) && contextBudgetValue is not null) + { + instance.ContextBudget = Convert.ToInt32(contextBudgetValue); + } + + if (data.TryGetValue("parallelToolCalls", out var parallelToolCallsValue) && parallelToolCallsValue is not null) + { + instance.ParallelToolCalls = Convert.ToBoolean(parallelToolCallsValue); + } + + if (data.TryGetValue("raw", out var rawValue) && rawValue is not null) + { + instance.Raw = Convert.ToBoolean(rawValue); + } + + if (data.TryGetValue("turn", out var turnValue) && turnValue is not null) + { + instance.Turn = Convert.ToInt32(turnValue); + } + + if (data.TryGetValue("compaction", out var compactionValue) && compactionValue is not null) + { + instance.Compaction = CompactionConfig.Load(compactionValue.GetDictionary(CompactionConfig.ShorthandProperty), context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + #endregion + + #region Save Methods + + /// + /// Save the TurnOptions instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + if (obj.MaxIterations is not null) + { + result["maxIterations"] = obj.MaxIterations; + } + + + if (obj.MaxLlmRetries is not null) + { + result["maxLlmRetries"] = obj.MaxLlmRetries; + } + + + if (obj.ContextBudget is not null) + { + result["contextBudget"] = obj.ContextBudget; + } + + + if (obj.ParallelToolCalls is not null) + { + result["parallelToolCalls"] = obj.ParallelToolCalls; + } + + + if (obj.Raw is not null) + { + result["raw"] = obj.Raw; + } + + + if (obj.Turn is not null) + { + result["turn"] = obj.Turn; + } + + + if (obj.Compaction is not null) + { + result["compaction"] = obj.Compaction?.Save(context); + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the TurnOptions instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the TurnOptions instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a TurnOptions instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TurnOptions instance. + public static TurnOptions FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a TurnOptions instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded TurnOptions instance. + public static TurnOptions FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/Prompty.Core/Pipeline.cs b/runtime/csharp/Prompty.Core/Pipeline.cs index 174bf065..26e2c358 100644 --- a/runtime/csharp/Prompty.Core/Pipeline.cs +++ b/runtime/csharp/Prompty.Core/Pipeline.cs @@ -76,14 +76,14 @@ public static async Task RenderAsync(Prompty agent, Dictionary /// Parse rendered text into a list of Messages. /// - public static async Task> ParseAsync(Prompty agent, string rendered) + public static async Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) { return await Trace.TraceAsync>("Prompty.Core.Pipeline.ParseAsync", async (emit) => { emit("inputs", new Dictionary { ["agent"] = agent.Name }); var parserKind = agent.Template?.Parser?.Kind ?? "prompty"; var parser = InvokerRegistry.GetParser(parserKind); - return await parser.ParseAsync(agent, rendered); + return await parser.ParseAsync(agent, rendered, null); }); } @@ -151,7 +151,7 @@ public static async Task> PrepareAsync( rendered = await RenderAsync(agent, validatedInputs); } - var messages = await parser.ParseAsync(agent, rendered); + var messages = await parser.ParseAsync(agent, rendered, parserContext); messages = ExpandThreadMarkers(messages, validatedInputs); return messages; }); @@ -769,7 +769,7 @@ internal static List ExpandThreadMarkers( { Role = msg.Role, Parts = [new TextPart { Value = before }], - Metadata = new Dictionary(msg.Metadata), + Metadata = msg.Metadata is not null ? new Dictionary(msg.Metadata) : null, }); } @@ -783,7 +783,7 @@ internal static List ExpandThreadMarkers( { Role = msg.Role, Parts = [new TextPart { Value = after }], - Metadata = new Dictionary(msg.Metadata), + Metadata = msg.Metadata is not null ? new Dictionary(msg.Metadata) : null, }); } } @@ -792,16 +792,6 @@ internal static List ExpandThreadMarkers( } } -/// -/// Represents a tool call requested by the LLM. -/// -public class ToolCall -{ - public string Id { get; set; } = ""; - public string Name { get; set; } = ""; - public string Arguments { get; set; } = ""; -} - /// /// Result from a processor indicating the LLM wants to call tools. /// Processors return this instead of a plain string when tool_calls are present. diff --git a/runtime/csharp/Prompty.Core/PromptyChatParser.cs b/runtime/csharp/Prompty.Core/PromptyChatParser.cs index 32df4234..3be861f4 100644 --- a/runtime/csharp/Prompty.Core/PromptyChatParser.cs +++ b/runtime/csharp/Prompty.Core/PromptyChatParser.cs @@ -71,7 +71,7 @@ public partial class PromptyChatParser : IParser, IPreRenderable // IParser // ----------------------------------------------------------------------- - public Task> ParseAsync(Prompty agent, string rendered) + public Task> ParseAsync(Prompty agent, string rendered, Dictionary? context) { var messages = Parse(rendered); return Task.FromResult(messages); @@ -160,6 +160,7 @@ private Message CreateMessage(string role, List contentLines, Dictionary if (attrs is not null && attrs.Count > 0) { + message.Metadata ??= new Dictionary(); foreach (var kvp in attrs) message.Metadata[kvp.Key] = kvp.Value; } diff --git a/runtime/csharp/Prompty.Core/Types.cs b/runtime/csharp/Prompty.Core/Types.cs index 66d87c04..4b5f38bc 100644 --- a/runtime/csharp/Prompty.Core/Types.cs +++ b/runtime/csharp/Prompty.Core/Types.cs @@ -30,101 +30,7 @@ public static class RichKinds }; } -/// -/// Base class for content parts within a message. -/// -public abstract class ContentPart -{ - public abstract string Kind { get; } -} - -/// -/// Plain text content. -/// -public class TextPart : ContentPart -{ - public override string Kind => "text"; - public string Value { get; set; } = ""; -} - -/// -/// Image content with source URL/base64 and optional detail level. -/// -public class ImagePart : ContentPart -{ - public override string Kind => "image"; - public string Source { get; set; } = ""; - public string? Detail { get; set; } - public string? MediaType { get; set; } -} - -/// -/// File content with source URL/path. -/// -public class FilePart : ContentPart -{ - public override string Kind => "file"; - public string Source { get; set; } = ""; - public string? MediaType { get; set; } -} - -/// -/// Audio content with source URL/base64. -/// -public class AudioPart : ContentPart -{ - public override string Kind => "audio"; - public string Source { get; set; } = ""; - public string? MediaType { get; set; } -} - -/// -/// A message in the LLM conversation with role, content parts, and metadata. -/// -public class Message -{ - public string Role { get; set; } = Roles.User; - public List Parts { get; set; } = []; - public Dictionary Metadata { get; set; } = []; - - /// - /// Concatenated text from all TextParts. - /// - public string Text => string.Join("", Parts.OfType().Select(p => p.Value)); - - /// - /// Returns the content as a simple string if only text parts exist, - /// or as a list of wire-format dictionaries for multimodal content. - /// - public object ToTextContent() - { - if (Parts.All(p => p is TextPart)) - return Text; - - return Parts.Select>(p => p switch - { - TextPart t => new() { ["type"] = "text", ["text"] = t.Value }, - ImagePart i => new() - { - ["type"] = "image_url", - ["image_url"] = new Dictionary - { - ["url"] = i.Source, - ["detail"] = i.Detail ?? "auto", - }, - }, - FilePart f => new() { ["type"] = "file", ["file"] = new Dictionary { ["url"] = f.Source } }, - AudioPart a => new() { ["type"] = "input_audio", ["input_audio"] = new Dictionary { ["url"] = a.Source } }, - _ => new() { ["type"] = p.Kind }, - }).ToList(); - } -} - /// /// Placeholder inserted by renderers to mark where thread/rich content should be expanded. /// -public class ThreadMarker -{ - public string Name { get; set; } = "thread"; - public string Kind { get; set; } = "thread"; -} +public partial class ThreadMarker { } diff --git a/runtime/csharp/Prompty.OpenAI/WireFormat.cs b/runtime/csharp/Prompty.OpenAI/WireFormat.cs index bef94fb1..156442bd 100644 --- a/runtime/csharp/Prompty.OpenAI/WireFormat.cs +++ b/runtime/csharp/Prompty.OpenAI/WireFormat.cs @@ -37,13 +37,13 @@ public static ChatMessage MessageToWire(Message msg) }, Roles.Assistant => BuildAssistantMessage(msg), Roles.Tool => new ToolChatMessage( - msg.Metadata.TryGetValue("tool_call_id", out var id) ? id?.ToString() ?? "" : "", + msg.Metadata?.TryGetValue("tool_call_id", out var id) == true ? id?.ToString() ?? "" : "", msg.Text), _ => new UserChatMessage(msg.Text), }; } - private static IEnumerable BuildContentParts(List parts) + private static IEnumerable BuildContentParts(IList parts) { foreach (var part in parts) { @@ -71,7 +71,7 @@ private static AssistantChatMessage BuildAssistantMessage(Message msg) var assistant = new AssistantChatMessage(msg.Text); // Attach tool calls from metadata if present - if (msg.Metadata.TryGetValue("tool_calls", out var tcObj) && tcObj is List toolCalls) + if (msg.Metadata is not null && msg.Metadata.TryGetValue("tool_calls", out var tcObj) && tcObj is List toolCalls) { foreach (var tc in toolCalls) { @@ -128,6 +128,8 @@ private static AssistantChatMessage BuildAssistantMessage(Message msg) /// /// Build ChatCompletionOptions from agent's ModelOptions. + /// Note: ModelOptions.ToWire("openai") is available for raw HTTP providers, + /// but this method uses the strongly-typed OpenAI SDK ChatCompletionOptions object. /// public static ChatCompletionOptions BuildOptions(Core.Prompty agent) { @@ -251,11 +253,11 @@ public static CreateResponseOptions BuildResponsesOptions(string model, Core.Pro public static ResponseItem? MessageToResponsesInput(Message msg) { // Pass through function call items stored in metadata - if (msg.Metadata.TryGetValue("responses_function_call", out var fcObj) && fcObj is ResponseItem fcItem) + if (msg.Metadata is not null && msg.Metadata.TryGetValue("responses_function_call", out var fcObj) && fcObj is ResponseItem fcItem) return fcItem; // Tool result → function_call_output - if (msg.Role == Roles.Tool && msg.Metadata.TryGetValue("tool_call_id", out var tcId)) + if (msg.Role == Roles.Tool && msg.Metadata is not null && msg.Metadata.TryGetValue("tool_call_id", out var tcId)) { return ResponseItem.CreateFunctionCallOutputItem( tcId?.ToString() ?? "", diff --git a/runtime/csharp/Prompty.Providers.Tests/AgentLoopTests.cs b/runtime/csharp/Prompty.Providers.Tests/AgentLoopTests.cs index 4d3fbb75..aa7026c3 100644 --- a/runtime/csharp/Prompty.Providers.Tests/AgentLoopTests.cs +++ b/runtime/csharp/Prompty.Providers.Tests/AgentLoopTests.cs @@ -262,7 +262,7 @@ public Task RenderAsync(Core.Prompty agent, string template, Dictionary< /// Parser that returns a single user message. private class PassthroughParser : IParser { - public Task> ParseAsync(Core.Prompty agent, string rendered) + public Task> ParseAsync(Core.Prompty agent, string rendered, Dictionary? context) => Task.FromResult>([new() { Role = Roles.User, Parts = [new TextPart { Value = rendered }] }]); } @@ -327,10 +327,10 @@ private static List DefaultFormatToolMessages(List toolCalls, { var messages = new List { - new() { Role = Roles.Assistant, Parts = string.IsNullOrEmpty(textContent) ? [] : [new TextPart { Value = textContent }], Metadata = new() { ["tool_calls"] = toolCalls } }, + new() { Role = Roles.Assistant, Parts = string.IsNullOrEmpty(textContent) ? [] : [new TextPart { Value = textContent }], Metadata = new Dictionary { ["tool_calls"] = toolCalls } }, }; for (var i = 0; i < toolCalls.Count; i++) - messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new() { ["tool_call_id"] = toolCalls[i].Id, ["name"] = toolCalls[i].Name } }); + messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new Dictionary { ["tool_call_id"] = toolCalls[i].Id, ["name"] = toolCalls[i].Name } }); return messages; } } diff --git a/runtime/csharp/Prompty.Providers.Tests/SpecVectorAgentTests.cs b/runtime/csharp/Prompty.Providers.Tests/SpecVectorAgentTests.cs index 92df5b0b..166d704c 100644 --- a/runtime/csharp/Prompty.Providers.Tests/SpecVectorAgentTests.cs +++ b/runtime/csharp/Prompty.Providers.Tests/SpecVectorAgentTests.cs @@ -696,7 +696,7 @@ public Task RenderAsync(Core.Prompty agent, string template, Dictionary< private class PassthroughParser : IParser { - public Task> ParseAsync(Core.Prompty agent, string rendered) + public Task> ParseAsync(Core.Prompty agent, string rendered, Dictionary? context) { var msgs = new List { @@ -722,10 +722,10 @@ public List FormatToolMessages(object rawResponse, List toolC { var messages = new List { - new() { Role = Roles.Assistant, Parts = [], Metadata = new() { ["tool_calls"] = toolCalls } }, + new() { Role = Roles.Assistant, Parts = [], Metadata = new Dictionary { ["tool_calls"] = toolCalls } }, }; for (var i = 0; i < toolCalls.Count; i++) - messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new() { ["tool_call_id"] = toolCalls[i].Id, ["name"] = toolCalls[i].Name } }); + messages.Add(new() { Role = Roles.Tool, Parts = [new TextPart { Value = toolResults[i] }], Metadata = new Dictionary { ["tool_call_id"] = toolCalls[i].Id, ["name"] = toolCalls[i].Name } }); return messages; } } diff --git a/runtime/csharp/Prompty.Providers.Tests/WireFormatTests.cs b/runtime/csharp/Prompty.Providers.Tests/WireFormatTests.cs index a84f67f2..1d0a0d8e 100644 --- a/runtime/csharp/Prompty.Providers.Tests/WireFormatTests.cs +++ b/runtime/csharp/Prompty.Providers.Tests/WireFormatTests.cs @@ -94,7 +94,7 @@ public void MessageToWire_AssistantWithToolCalls_PreservesToolCalls() { Role = Roles.Assistant, Parts = [new TextPart { Value = "" }], - Metadata = new() + Metadata = new Dictionary { ["tool_calls"] = new List { @@ -116,7 +116,7 @@ public void MessageToWire_ToolMessage_ReturnsToolChatMessage() { Role = Roles.Tool, Parts = [new TextPart { Value = "72°F and sunny" }], - Metadata = new() { ["tool_call_id"] = "call_1" }, + Metadata = new Dictionary { ["tool_call_id"] = "call_1" }, }; var result = WireFormat.MessageToWire(msg); diff --git a/runtime/go/prompty/go.mod b/runtime/go/prompty/go.mod new file mode 100644 index 00000000..3a16a34f --- /dev/null +++ b/runtime/go/prompty/go.mod @@ -0,0 +1,5 @@ +module prompty + +go 1.25.1 + +require gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/runtime/go/prompty/go.sum b/runtime/go/prompty/go.sum new file mode 100644 index 00000000..4bc03378 --- /dev/null +++ b/runtime/go/prompty/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/runtime/go/prompty/model/anonymous_connection_test.go b/runtime/go/prompty/model/anonymous_connection_test.go index 26e651ce..554368e4 100644 --- a/runtime/go/prompty/model/anonymous_connection_test.go +++ b/runtime/go/prompty/model/anonymous_connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestAnonymousConnectionLoadJSON tests loading AnonymousConnection from JSON diff --git a/runtime/go/prompty/model/api_key_connection_test.go b/runtime/go/prompty/model/api_key_connection_test.go index 673f18b3..31684916 100644 --- a/runtime/go/prompty/model/api_key_connection_test.go +++ b/runtime/go/prompty/model/api_key_connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestApiKeyConnectionLoadJSON tests loading ApiKeyConnection from JSON diff --git a/runtime/go/prompty/model/array_property_test.go b/runtime/go/prompty/model/array_property_test.go index 6b1bc62c..8722b36a 100644 --- a/runtime/go/prompty/model/array_property_test.go +++ b/runtime/go/prompty/model/array_property_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestArrayPropertyLoadJSON tests loading ArrayProperty from JSON diff --git a/runtime/go/prompty/model/audio_part_test.go b/runtime/go/prompty/model/audio_part_test.go new file mode 100644 index 00000000..acca1517 --- /dev/null +++ b/runtime/go/prompty/model/audio_part_test.go @@ -0,0 +1,154 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestAudioPartLoadJSON tests loading AudioPart from JSON +func TestAudioPartLoadJSON(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadAudioPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load AudioPart: %v", err) + } + if instance.Source != "https://example.com/audio.wav" { + t.Errorf(`Expected Source to be "https://example.com/audio.wav", got %v`, instance.Source) + } + if instance.MediaType == nil || *instance.MediaType != "audio/wav" { + t.Errorf(`Expected MediaType to be "audio/wav", got %v`, instance.MediaType) + } +} + +// TestAudioPartLoadYAML tests loading AudioPart from YAML +func TestAudioPartLoadYAML(t *testing.T) { + yamlData := ` +source: "https://example.com/audio.wav" +mediaType: audio/wav + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadAudioPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load AudioPart: %v", err) + } + if instance.Source != "https://example.com/audio.wav" { + t.Errorf(`Expected Source to be "https://example.com/audio.wav", got %v`, instance.Source) + } + if instance.MediaType == nil || *instance.MediaType != "audio/wav" { + t.Errorf(`Expected MediaType to be "audio/wav", got %v`, instance.MediaType) + } +} + +// TestAudioPartRoundtrip tests load -> save -> load produces equivalent data +func TestAudioPartRoundtrip(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadAudioPart(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load AudioPart: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadAudioPart(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload AudioPart: %v", err) + } + if reloaded.Source != "https://example.com/audio.wav" { + t.Errorf(`Expected Source to be "https://example.com/audio.wav", got %v`, reloaded.Source) + } + if reloaded.MediaType == nil || *reloaded.MediaType != "audio/wav" { + t.Errorf(`Expected MediaType to be "audio/wav", got %v`, reloaded.MediaType) + } +} + +// TestAudioPartToJSON tests that ToJSON produces valid JSON +func TestAudioPartToJSON(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadAudioPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load AudioPart: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestAudioPartToYAML tests that ToYAML produces valid YAML +func TestAudioPartToYAML(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadAudioPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load AudioPart: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/binding_test.go b/runtime/go/prompty/model/binding_test.go index 0fc94143..7362ea71 100644 --- a/runtime/go/prompty/model/binding_test.go +++ b/runtime/go/prompty/model/binding_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestBindingLoadJSON tests loading Binding from JSON diff --git a/runtime/go/prompty/model/compaction_complete_payload.go b/runtime/go/prompty/model/compaction_complete_payload.go new file mode 100644 index 00000000..722ebeb9 --- /dev/null +++ b/runtime/go/prompty/model/compaction_complete_payload.go @@ -0,0 +1,106 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// CompactionCompletePayload represents Payload for "compaction_complete" events — context compaction finished. + +type CompactionCompletePayload struct { + Removed int32 `json:"removed" yaml:"removed"` + Remaining int32 `json:"remaining" yaml:"remaining"` +} + +// LoadCompactionCompletePayload creates a CompactionCompletePayload from a map[string]interface{} +func LoadCompactionCompletePayload(data interface{}, ctx *LoadContext) (CompactionCompletePayload, error) { + result := CompactionCompletePayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["removed"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.Removed = v + } + if val, ok := m["remaining"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.Remaining = v + } + } + + return result, nil +} + +// Save serializes CompactionCompletePayload to map[string]interface{} +func (obj *CompactionCompletePayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["removed"] = obj.Removed + result["remaining"] = obj.Remaining + + return result +} + +// ToJSON serializes CompactionCompletePayload to JSON string +func (obj *CompactionCompletePayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes CompactionCompletePayload to YAML string +func (obj *CompactionCompletePayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates CompactionCompletePayload from JSON string +func CompactionCompletePayloadFromJSON(jsonStr string) (CompactionCompletePayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return CompactionCompletePayload{}, err + } + ctx := NewLoadContext() + return LoadCompactionCompletePayload(data, ctx) +} + +// FromYAML creates CompactionCompletePayload from YAML string +func CompactionCompletePayloadFromYAML(yamlStr string) (CompactionCompletePayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return CompactionCompletePayload{}, err + } + ctx := NewLoadContext() + return LoadCompactionCompletePayload(data, ctx) +} diff --git a/runtime/go/prompty/model/compaction_complete_payload_test.go b/runtime/go/prompty/model/compaction_complete_payload_test.go new file mode 100644 index 00000000..154b92ea --- /dev/null +++ b/runtime/go/prompty/model/compaction_complete_payload_test.go @@ -0,0 +1,154 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestCompactionCompletePayloadLoadJSON tests loading CompactionCompletePayload from JSON +func TestCompactionCompletePayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "removed": 5, + "remaining": 3 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionCompletePayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionCompletePayload: %v", err) + } + if instance.Removed != 5 { + t.Errorf(`Expected Removed to be 5, got %v`, instance.Removed) + } + if instance.Remaining != 3 { + t.Errorf(`Expected Remaining to be 3, got %v`, instance.Remaining) + } +} + +// TestCompactionCompletePayloadLoadYAML tests loading CompactionCompletePayload from YAML +func TestCompactionCompletePayloadLoadYAML(t *testing.T) { + yamlData := ` +removed: 5 +remaining: 3 + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionCompletePayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionCompletePayload: %v", err) + } + if instance.Removed != 5 { + t.Errorf(`Expected Removed to be 5, got %v`, instance.Removed) + } + if instance.Remaining != 3 { + t.Errorf(`Expected Remaining to be 3, got %v`, instance.Remaining) + } +} + +// TestCompactionCompletePayloadRoundtrip tests load -> save -> load produces equivalent data +func TestCompactionCompletePayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "removed": 5, + "remaining": 3 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionCompletePayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load CompactionCompletePayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadCompactionCompletePayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload CompactionCompletePayload: %v", err) + } + if reloaded.Removed != 5 { + t.Errorf(`Expected Removed to be 5, got %v`, reloaded.Removed) + } + if reloaded.Remaining != 3 { + t.Errorf(`Expected Remaining to be 3, got %v`, reloaded.Remaining) + } +} + +// TestCompactionCompletePayloadToJSON tests that ToJSON produces valid JSON +func TestCompactionCompletePayloadToJSON(t *testing.T) { + jsonData := ` +{ + "removed": 5, + "remaining": 3 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionCompletePayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionCompletePayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestCompactionCompletePayloadToYAML tests that ToYAML produces valid YAML +func TestCompactionCompletePayloadToYAML(t *testing.T) { + jsonData := ` +{ + "removed": 5, + "remaining": 3 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionCompletePayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionCompletePayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/compaction_config.go b/runtime/go/prompty/model/compaction_config.go new file mode 100644 index 00000000..792d7ef3 --- /dev/null +++ b/runtime/go/prompty/model/compaction_config.go @@ -0,0 +1,111 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// CompactionConfig represents Configuration for context window compaction. When the message history +// exceeds the context budget, the compaction strategy is applied to +// reduce the message list while preserving essential information. + +type CompactionConfig struct { + Strategy *string `json:"strategy,omitempty" yaml:"strategy,omitempty"` + Budget *int32 `json:"budget,omitempty" yaml:"budget,omitempty"` + Options map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` +} + +// LoadCompactionConfig creates a CompactionConfig from a map[string]interface{} +func LoadCompactionConfig(data interface{}, ctx *LoadContext) (CompactionConfig, error) { + result := CompactionConfig{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["strategy"]; ok && val != nil { + v := val.(string) + result.Strategy = &v + } + if val, ok := m["budget"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.Budget = &v + } + if val, ok := m["options"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + result.Options = m + } + } + } + + return result, nil +} + +// Save serializes CompactionConfig to map[string]interface{} +func (obj *CompactionConfig) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + if obj.Strategy != nil { + result["strategy"] = *obj.Strategy + } + if obj.Budget != nil { + result["budget"] = *obj.Budget + } + if obj.Options != nil { + result["options"] = obj.Options + } + + return result +} + +// ToJSON serializes CompactionConfig to JSON string +func (obj *CompactionConfig) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes CompactionConfig to YAML string +func (obj *CompactionConfig) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates CompactionConfig from JSON string +func CompactionConfigFromJSON(jsonStr string) (CompactionConfig, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return CompactionConfig{}, err + } + ctx := NewLoadContext() + return LoadCompactionConfig(data, ctx) +} + +// FromYAML creates CompactionConfig from YAML string +func CompactionConfigFromYAML(yamlStr string) (CompactionConfig, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return CompactionConfig{}, err + } + ctx := NewLoadContext() + return LoadCompactionConfig(data, ctx) +} diff --git a/runtime/go/prompty/model/compaction_config_test.go b/runtime/go/prompty/model/compaction_config_test.go new file mode 100644 index 00000000..3116bbb9 --- /dev/null +++ b/runtime/go/prompty/model/compaction_config_test.go @@ -0,0 +1,168 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestCompactionConfigLoadJSON tests loading CompactionConfig from JSON +func TestCompactionConfigLoadJSON(t *testing.T) { + jsonData := ` +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionConfig(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionConfig: %v", err) + } + if instance.Strategy == nil || *instance.Strategy != "summarize" { + t.Errorf(`Expected Strategy to be "summarize", got %v`, instance.Strategy) + } + if instance.Budget == nil || *instance.Budget != 50000 { + t.Errorf(`Expected Budget to be 50000, got %v`, instance.Budget) + } +} + +// TestCompactionConfigLoadYAML tests loading CompactionConfig from YAML +func TestCompactionConfigLoadYAML(t *testing.T) { + yamlData := ` +strategy: summarize +budget: 50000 +options: + preserveSystemMessages: true + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionConfig(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionConfig: %v", err) + } + if instance.Strategy == nil || *instance.Strategy != "summarize" { + t.Errorf(`Expected Strategy to be "summarize", got %v`, instance.Strategy) + } + if instance.Budget == nil || *instance.Budget != 50000 { + t.Errorf(`Expected Budget to be 50000, got %v`, instance.Budget) + } +} + +// TestCompactionConfigRoundtrip tests load -> save -> load produces equivalent data +func TestCompactionConfigRoundtrip(t *testing.T) { + jsonData := ` +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionConfig(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load CompactionConfig: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadCompactionConfig(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload CompactionConfig: %v", err) + } + if reloaded.Strategy == nil || *reloaded.Strategy != "summarize" { + t.Errorf(`Expected Strategy to be "summarize", got %v`, reloaded.Strategy) + } + if reloaded.Budget == nil || *reloaded.Budget != 50000 { + t.Errorf(`Expected Budget to be 50000, got %v`, reloaded.Budget) + } +} + +// TestCompactionConfigToJSON tests that ToJSON produces valid JSON +func TestCompactionConfigToJSON(t *testing.T) { + jsonData := ` +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionConfig(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionConfig: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestCompactionConfigToYAML tests that ToYAML produces valid YAML +func TestCompactionConfigToYAML(t *testing.T) { + jsonData := ` +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionConfig(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionConfig: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/compaction_failed_payload.go b/runtime/go/prompty/model/compaction_failed_payload.go new file mode 100644 index 00000000..4e79f2c3 --- /dev/null +++ b/runtime/go/prompty/model/compaction_failed_payload.go @@ -0,0 +1,79 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// CompactionFailedPayload represents Payload for "compaction_failed" events — compaction could not be completed. + +type CompactionFailedPayload struct { + Message string `json:"message" yaml:"message"` +} + +// LoadCompactionFailedPayload creates a CompactionFailedPayload from a map[string]interface{} +func LoadCompactionFailedPayload(data interface{}, ctx *LoadContext) (CompactionFailedPayload, error) { + result := CompactionFailedPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["message"]; ok && val != nil { + result.Message = val.(string) + } + } + + return result, nil +} + +// Save serializes CompactionFailedPayload to map[string]interface{} +func (obj *CompactionFailedPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["message"] = obj.Message + + return result +} + +// ToJSON serializes CompactionFailedPayload to JSON string +func (obj *CompactionFailedPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes CompactionFailedPayload to YAML string +func (obj *CompactionFailedPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates CompactionFailedPayload from JSON string +func CompactionFailedPayloadFromJSON(jsonStr string) (CompactionFailedPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return CompactionFailedPayload{}, err + } + ctx := NewLoadContext() + return LoadCompactionFailedPayload(data, ctx) +} + +// FromYAML creates CompactionFailedPayload from YAML string +func CompactionFailedPayloadFromYAML(yamlStr string) (CompactionFailedPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return CompactionFailedPayload{}, err + } + ctx := NewLoadContext() + return LoadCompactionFailedPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/compaction_failed_payload_test.go b/runtime/go/prompty/model/compaction_failed_payload_test.go new file mode 100644 index 00000000..75330125 --- /dev/null +++ b/runtime/go/prompty/model/compaction_failed_payload_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestCompactionFailedPayloadLoadJSON tests loading CompactionFailedPayload from JSON +func TestCompactionFailedPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "message": "Summarization prompt exceeded context window" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionFailedPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionFailedPayload: %v", err) + } + if instance.Message != "Summarization prompt exceeded context window" { + t.Errorf(`Expected Message to be "Summarization prompt exceeded context window", got %v`, instance.Message) + } +} + +// TestCompactionFailedPayloadLoadYAML tests loading CompactionFailedPayload from YAML +func TestCompactionFailedPayloadLoadYAML(t *testing.T) { + yamlData := ` +message: Summarization prompt exceeded context window + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionFailedPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionFailedPayload: %v", err) + } + if instance.Message != "Summarization prompt exceeded context window" { + t.Errorf(`Expected Message to be "Summarization prompt exceeded context window", got %v`, instance.Message) + } +} + +// TestCompactionFailedPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestCompactionFailedPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "message": "Summarization prompt exceeded context window" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionFailedPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load CompactionFailedPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadCompactionFailedPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload CompactionFailedPayload: %v", err) + } + if reloaded.Message != "Summarization prompt exceeded context window" { + t.Errorf(`Expected Message to be "Summarization prompt exceeded context window", got %v`, reloaded.Message) + } +} + +// TestCompactionFailedPayloadToJSON tests that ToJSON produces valid JSON +func TestCompactionFailedPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "message": "Summarization prompt exceeded context window" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionFailedPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionFailedPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestCompactionFailedPayloadToYAML tests that ToYAML produces valid YAML +func TestCompactionFailedPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "message": "Summarization prompt exceeded context window" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadCompactionFailedPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load CompactionFailedPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/connection.go b/runtime/go/prompty/model/connection.go index 3a585211..338c2558 100644 --- a/runtime/go/prompty/model/connection.go +++ b/runtime/go/prompty/model/connection.go @@ -35,10 +35,10 @@ func LoadConnection(data interface{}, ctx *LoadContext) (interface{}, error) { return LoadApiKeyConnection(data, ctx) case "anonymous": return LoadAnonymousConnection(data, ctx) - case "foundry": - return LoadFoundryConnection(data, ctx) case "oauth": return LoadOAuthConnection(data, ctx) + case "foundry": + return LoadFoundryConnection(data, ctx) } } } @@ -435,20 +435,22 @@ func AnonymousConnectionFromYAML(yamlStr string) (AnonymousConnection, error) { return LoadAnonymousConnection(data, ctx) } -// FoundryConnection represents Connection configuration for Microsoft Foundry projects. -// Provides project-scoped access to models, tools, and services -// via Entra ID (DefaultAzureCredential) authentication. +// OAuthConnection represents Connection configuration using OAuth 2.0 client credentials. +// Useful for tools and services that require OAuth authentication, +// such as MCP servers, OpenAPI endpoints, or other REST APIs. -type FoundryConnection struct { - Kind string `json:"kind" yaml:"kind"` - Endpoint string `json:"endpoint" yaml:"endpoint"` - Name *string `json:"name,omitempty" yaml:"name,omitempty"` - ConnectionType *string `json:"connectionType,omitempty" yaml:"connectionType,omitempty"` +type OAuthConnection struct { + Kind string `json:"kind" yaml:"kind"` + Endpoint string `json:"endpoint" yaml:"endpoint"` + ClientId string `json:"clientId" yaml:"clientId"` + ClientSecret string `json:"clientSecret" yaml:"clientSecret"` + TokenUrl string `json:"tokenUrl" yaml:"tokenUrl"` + Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` } -// LoadFoundryConnection creates a FoundryConnection from a map[string]interface{} -func LoadFoundryConnection(data interface{}, ctx *LoadContext) (FoundryConnection, error) { - result := FoundryConnection{} +// LoadOAuthConnection creates a OAuthConnection from a map[string]interface{} +func LoadOAuthConnection(data interface{}, ctx *LoadContext) (OAuthConnection, error) { + result := OAuthConnection{} // Load from map if m, ok := data.(map[string]interface{}); ok { @@ -458,36 +460,43 @@ func LoadFoundryConnection(data interface{}, ctx *LoadContext) (FoundryConnectio if val, ok := m["endpoint"]; ok && val != nil { result.Endpoint = val.(string) } - if val, ok := m["name"]; ok && val != nil { - v := val.(string) - result.Name = &v + if val, ok := m["clientId"]; ok && val != nil { + result.ClientId = val.(string) } - if val, ok := m["connectionType"]; ok && val != nil { - v := val.(string) - result.ConnectionType = &v + if val, ok := m["clientSecret"]; ok && val != nil { + result.ClientSecret = val.(string) + } + if val, ok := m["tokenUrl"]; ok && val != nil { + result.TokenUrl = val.(string) + } + if val, ok := m["scopes"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Scopes = make([]string, len(arr)) + for i, v := range arr { + result.Scopes[i] = v.(string) + } + } } } return result, nil } -// Save serializes FoundryConnection to map[string]interface{} -func (obj *FoundryConnection) Save(ctx *SaveContext) map[string]interface{} { +// Save serializes OAuthConnection to map[string]interface{} +func (obj *OAuthConnection) Save(ctx *SaveContext) map[string]interface{} { result := make(map[string]interface{}) result["kind"] = obj.Kind result["endpoint"] = obj.Endpoint - if obj.Name != nil { - result["name"] = *obj.Name - } - if obj.ConnectionType != nil { - result["connectionType"] = *obj.ConnectionType - } + result["clientId"] = obj.ClientId + result["clientSecret"] = obj.ClientSecret + result["tokenUrl"] = obj.TokenUrl + result["scopes"] = obj.Scopes return result } -// ToJSON serializes FoundryConnection to JSON string -func (obj *FoundryConnection) ToJSON() (string, error) { +// ToJSON serializes OAuthConnection to JSON string +func (obj *OAuthConnection) ToJSON() (string, error) { ctx := NewSaveContext() data := obj.Save(ctx) bytes, err := json.Marshal(data) @@ -497,8 +506,8 @@ func (obj *FoundryConnection) ToJSON() (string, error) { return string(bytes), nil } -// ToYAML serializes FoundryConnection to YAML string -func (obj *FoundryConnection) ToYAML() (string, error) { +// ToYAML serializes OAuthConnection to YAML string +func (obj *OAuthConnection) ToYAML() (string, error) { ctx := NewSaveContext() data := obj.Save(ctx) bytes, err := yaml.Marshal(data) @@ -508,42 +517,40 @@ func (obj *FoundryConnection) ToYAML() (string, error) { return string(bytes), nil } -// FromJSON creates FoundryConnection from JSON string -func FoundryConnectionFromJSON(jsonStr string) (FoundryConnection, error) { +// FromJSON creates OAuthConnection from JSON string +func OAuthConnectionFromJSON(jsonStr string) (OAuthConnection, error) { var data map[string]interface{} if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { - return FoundryConnection{}, err + return OAuthConnection{}, err } ctx := NewLoadContext() - return LoadFoundryConnection(data, ctx) + return LoadOAuthConnection(data, ctx) } -// FromYAML creates FoundryConnection from YAML string -func FoundryConnectionFromYAML(yamlStr string) (FoundryConnection, error) { +// FromYAML creates OAuthConnection from YAML string +func OAuthConnectionFromYAML(yamlStr string) (OAuthConnection, error) { var data map[string]interface{} if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { - return FoundryConnection{}, err + return OAuthConnection{}, err } ctx := NewLoadContext() - return LoadFoundryConnection(data, ctx) + return LoadOAuthConnection(data, ctx) } -// OAuthConnection represents Connection configuration using OAuth 2.0 client credentials. -// Useful for tools and services that require OAuth authentication, -// such as MCP servers, OpenAPI endpoints, or other REST APIs. +// FoundryConnection represents Connection configuration for Microsoft Foundry projects. +// Provides project-scoped access to models, tools, and services +// via Entra ID (DefaultAzureCredential) authentication. -type OAuthConnection struct { - Kind string `json:"kind" yaml:"kind"` - Endpoint string `json:"endpoint" yaml:"endpoint"` - ClientId string `json:"clientId" yaml:"clientId"` - ClientSecret string `json:"clientSecret" yaml:"clientSecret"` - TokenUrl string `json:"tokenUrl" yaml:"tokenUrl"` - Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` +type FoundryConnection struct { + Kind string `json:"kind" yaml:"kind"` + Endpoint string `json:"endpoint" yaml:"endpoint"` + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + ConnectionType *string `json:"connectionType,omitempty" yaml:"connectionType,omitempty"` } -// LoadOAuthConnection creates a OAuthConnection from a map[string]interface{} -func LoadOAuthConnection(data interface{}, ctx *LoadContext) (OAuthConnection, error) { - result := OAuthConnection{} +// LoadFoundryConnection creates a FoundryConnection from a map[string]interface{} +func LoadFoundryConnection(data interface{}, ctx *LoadContext) (FoundryConnection, error) { + result := FoundryConnection{} // Load from map if m, ok := data.(map[string]interface{}); ok { @@ -553,43 +560,36 @@ func LoadOAuthConnection(data interface{}, ctx *LoadContext) (OAuthConnection, e if val, ok := m["endpoint"]; ok && val != nil { result.Endpoint = val.(string) } - if val, ok := m["clientId"]; ok && val != nil { - result.ClientId = val.(string) - } - if val, ok := m["clientSecret"]; ok && val != nil { - result.ClientSecret = val.(string) - } - if val, ok := m["tokenUrl"]; ok && val != nil { - result.TokenUrl = val.(string) + if val, ok := m["name"]; ok && val != nil { + v := val.(string) + result.Name = &v } - if val, ok := m["scopes"]; ok && val != nil { - if arr, ok := val.([]interface{}); ok { - result.Scopes = make([]string, len(arr)) - for i, v := range arr { - result.Scopes[i] = v.(string) - } - } + if val, ok := m["connectionType"]; ok && val != nil { + v := val.(string) + result.ConnectionType = &v } } return result, nil } -// Save serializes OAuthConnection to map[string]interface{} -func (obj *OAuthConnection) Save(ctx *SaveContext) map[string]interface{} { +// Save serializes FoundryConnection to map[string]interface{} +func (obj *FoundryConnection) Save(ctx *SaveContext) map[string]interface{} { result := make(map[string]interface{}) result["kind"] = obj.Kind result["endpoint"] = obj.Endpoint - result["clientId"] = obj.ClientId - result["clientSecret"] = obj.ClientSecret - result["tokenUrl"] = obj.TokenUrl - result["scopes"] = obj.Scopes + if obj.Name != nil { + result["name"] = *obj.Name + } + if obj.ConnectionType != nil { + result["connectionType"] = *obj.ConnectionType + } return result } -// ToJSON serializes OAuthConnection to JSON string -func (obj *OAuthConnection) ToJSON() (string, error) { +// ToJSON serializes FoundryConnection to JSON string +func (obj *FoundryConnection) ToJSON() (string, error) { ctx := NewSaveContext() data := obj.Save(ctx) bytes, err := json.Marshal(data) @@ -599,8 +599,8 @@ func (obj *OAuthConnection) ToJSON() (string, error) { return string(bytes), nil } -// ToYAML serializes OAuthConnection to YAML string -func (obj *OAuthConnection) ToYAML() (string, error) { +// ToYAML serializes FoundryConnection to YAML string +func (obj *FoundryConnection) ToYAML() (string, error) { ctx := NewSaveContext() data := obj.Save(ctx) bytes, err := yaml.Marshal(data) @@ -610,22 +610,22 @@ func (obj *OAuthConnection) ToYAML() (string, error) { return string(bytes), nil } -// FromJSON creates OAuthConnection from JSON string -func OAuthConnectionFromJSON(jsonStr string) (OAuthConnection, error) { +// FromJSON creates FoundryConnection from JSON string +func FoundryConnectionFromJSON(jsonStr string) (FoundryConnection, error) { var data map[string]interface{} if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { - return OAuthConnection{}, err + return FoundryConnection{}, err } ctx := NewLoadContext() - return LoadOAuthConnection(data, ctx) + return LoadFoundryConnection(data, ctx) } -// FromYAML creates OAuthConnection from YAML string -func OAuthConnectionFromYAML(yamlStr string) (OAuthConnection, error) { +// FromYAML creates FoundryConnection from YAML string +func FoundryConnectionFromYAML(yamlStr string) (FoundryConnection, error) { var data map[string]interface{} if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { - return OAuthConnection{}, err + return FoundryConnection{}, err } ctx := NewLoadContext() - return LoadOAuthConnection(data, ctx) + return LoadFoundryConnection(data, ctx) } diff --git a/runtime/go/prompty/model/connection_test.go b/runtime/go/prompty/model/connection_test.go index d37de4bd..742c9bc2 100644 --- a/runtime/go/prompty/model/connection_test.go +++ b/runtime/go/prompty/model/connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestConnectionLoadJSON tests loading Connection from JSON diff --git a/runtime/go/prompty/model/content_part.go b/runtime/go/prompty/model/content_part.go new file mode 100644 index 00000000..45d65e3f --- /dev/null +++ b/runtime/go/prompty/model/content_part.go @@ -0,0 +1,430 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ContentPart represents A part of a message's content. Content parts are discriminated on the `kind` +// field and represent the different modalities that can appear in a message. + +type ContentPart struct { + Kind string `json:"kind" yaml:"kind"` +} + +// LoadContentPart creates a ContentPart from a map[string]interface{} +// Returns interface{} because this is a polymorphic base type that can resolve to different child types +func LoadContentPart(data interface{}, ctx *LoadContext) (interface{}, error) { + result := ContentPart{} + + // Handle polymorphic types based on discriminator + if m, ok := data.(map[string]interface{}); ok { + if discriminator, ok := m["kind"]; ok { + switch discriminator { + case "text": + return LoadTextPart(data, ctx) + case "image": + return LoadImagePart(data, ctx) + case "file": + return LoadFilePart(data, ctx) + case "audio": + return LoadAudioPart(data, ctx) + } + } + } + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + } + + return result, nil +} + +// Save serializes ContentPart to map[string]interface{} +func (obj *ContentPart) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + + return result +} + +// ToJSON serializes ContentPart to JSON string +func (obj *ContentPart) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ContentPart to YAML string +func (obj *ContentPart) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ContentPart from JSON string +// Returns interface{} because this is a polymorphic base type that can resolve to different child types +func ContentPartFromJSON(jsonStr string) (interface{}, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return nil, err + } + ctx := NewLoadContext() + return LoadContentPart(data, ctx) +} + +// FromYAML creates ContentPart from YAML string +// Returns interface{} because this is a polymorphic base type that can resolve to different child types +func ContentPartFromYAML(yamlStr string) (interface{}, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return nil, err + } + ctx := NewLoadContext() + return LoadContentPart(data, ctx) +} + +// TextPart represents A text content part. + +type TextPart struct { + Kind string `json:"kind" yaml:"kind"` + Value string `json:"value" yaml:"value"` +} + +// LoadTextPart creates a TextPart from a map[string]interface{} +func LoadTextPart(data interface{}, ctx *LoadContext) (TextPart, error) { + result := TextPart{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["value"]; ok && val != nil { + result.Value = val.(string) + } + } + + return result, nil +} + +// Save serializes TextPart to map[string]interface{} +func (obj *TextPart) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["value"] = obj.Value + + return result +} + +// ToJSON serializes TextPart to JSON string +func (obj *TextPart) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes TextPart to YAML string +func (obj *TextPart) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates TextPart from JSON string +func TextPartFromJSON(jsonStr string) (TextPart, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return TextPart{}, err + } + ctx := NewLoadContext() + return LoadTextPart(data, ctx) +} + +// FromYAML creates TextPart from YAML string +func TextPartFromYAML(yamlStr string) (TextPart, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return TextPart{}, err + } + ctx := NewLoadContext() + return LoadTextPart(data, ctx) +} + +// ImagePart represents An image content part. The source may be a URL or base64-encoded data. + +type ImagePart struct { + Kind string `json:"kind" yaml:"kind"` + Source string `json:"source" yaml:"source"` + Detail *string `json:"detail,omitempty" yaml:"detail,omitempty"` + MediaType *string `json:"mediaType,omitempty" yaml:"mediaType,omitempty"` +} + +// LoadImagePart creates a ImagePart from a map[string]interface{} +func LoadImagePart(data interface{}, ctx *LoadContext) (ImagePart, error) { + result := ImagePart{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["source"]; ok && val != nil { + result.Source = val.(string) + } + if val, ok := m["detail"]; ok && val != nil { + v := val.(string) + result.Detail = &v + } + if val, ok := m["mediaType"]; ok && val != nil { + v := val.(string) + result.MediaType = &v + } + } + + return result, nil +} + +// Save serializes ImagePart to map[string]interface{} +func (obj *ImagePart) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["source"] = obj.Source + if obj.Detail != nil { + result["detail"] = *obj.Detail + } + if obj.MediaType != nil { + result["mediaType"] = *obj.MediaType + } + + return result +} + +// ToJSON serializes ImagePart to JSON string +func (obj *ImagePart) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ImagePart to YAML string +func (obj *ImagePart) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ImagePart from JSON string +func ImagePartFromJSON(jsonStr string) (ImagePart, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ImagePart{}, err + } + ctx := NewLoadContext() + return LoadImagePart(data, ctx) +} + +// FromYAML creates ImagePart from YAML string +func ImagePartFromYAML(yamlStr string) (ImagePart, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ImagePart{}, err + } + ctx := NewLoadContext() + return LoadImagePart(data, ctx) +} + +// FilePart represents A file content part. The source may be a URL or base64-encoded data. + +type FilePart struct { + Kind string `json:"kind" yaml:"kind"` + Source string `json:"source" yaml:"source"` + MediaType *string `json:"mediaType,omitempty" yaml:"mediaType,omitempty"` +} + +// LoadFilePart creates a FilePart from a map[string]interface{} +func LoadFilePart(data interface{}, ctx *LoadContext) (FilePart, error) { + result := FilePart{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["source"]; ok && val != nil { + result.Source = val.(string) + } + if val, ok := m["mediaType"]; ok && val != nil { + v := val.(string) + result.MediaType = &v + } + } + + return result, nil +} + +// Save serializes FilePart to map[string]interface{} +func (obj *FilePart) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["source"] = obj.Source + if obj.MediaType != nil { + result["mediaType"] = *obj.MediaType + } + + return result +} + +// ToJSON serializes FilePart to JSON string +func (obj *FilePart) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes FilePart to YAML string +func (obj *FilePart) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates FilePart from JSON string +func FilePartFromJSON(jsonStr string) (FilePart, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return FilePart{}, err + } + ctx := NewLoadContext() + return LoadFilePart(data, ctx) +} + +// FromYAML creates FilePart from YAML string +func FilePartFromYAML(yamlStr string) (FilePart, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return FilePart{}, err + } + ctx := NewLoadContext() + return LoadFilePart(data, ctx) +} + +// AudioPart represents An audio content part. The source may be a URL or base64-encoded data. + +type AudioPart struct { + Kind string `json:"kind" yaml:"kind"` + Source string `json:"source" yaml:"source"` + MediaType *string `json:"mediaType,omitempty" yaml:"mediaType,omitempty"` +} + +// LoadAudioPart creates a AudioPart from a map[string]interface{} +func LoadAudioPart(data interface{}, ctx *LoadContext) (AudioPart, error) { + result := AudioPart{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["source"]; ok && val != nil { + result.Source = val.(string) + } + if val, ok := m["mediaType"]; ok && val != nil { + v := val.(string) + result.MediaType = &v + } + } + + return result, nil +} + +// Save serializes AudioPart to map[string]interface{} +func (obj *AudioPart) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["source"] = obj.Source + if obj.MediaType != nil { + result["mediaType"] = *obj.MediaType + } + + return result +} + +// ToJSON serializes AudioPart to JSON string +func (obj *AudioPart) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes AudioPart to YAML string +func (obj *AudioPart) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates AudioPart from JSON string +func AudioPartFromJSON(jsonStr string) (AudioPart, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return AudioPart{}, err + } + ctx := NewLoadContext() + return LoadAudioPart(data, ctx) +} + +// FromYAML creates AudioPart from YAML string +func AudioPartFromYAML(yamlStr string) (AudioPart, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return AudioPart{}, err + } + ctx := NewLoadContext() + return LoadAudioPart(data, ctx) +} diff --git a/runtime/go/prompty/model/content_part_test.go b/runtime/go/prompty/model/content_part_test.go new file mode 100644 index 00000000..8e719bf5 --- /dev/null +++ b/runtime/go/prompty/model/content_part_test.go @@ -0,0 +1,3 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test diff --git a/runtime/go/prompty/model/context.go b/runtime/go/prompty/model/context.go index 8152d3b7..9d70e8b5 100644 --- a/runtime/go/prompty/model/context.go +++ b/runtime/go/prompty/model/context.go @@ -24,3 +24,9 @@ type SaveContext struct { func NewSaveContext() *SaveContext { return &SaveContext{} } + +// ptrOf returns a pointer to the given value. Used by factory functions +// to set optional (pointer) fields in struct literals. +func ptrOf[T any](v T) *T { + return &v +} diff --git a/runtime/go/prompty/model/custom_tool_test.go b/runtime/go/prompty/model/custom_tool_test.go index 99f98f47..04148337 100644 --- a/runtime/go/prompty/model/custom_tool_test.go +++ b/runtime/go/prompty/model/custom_tool_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestCustomToolLoadJSON tests loading CustomTool from JSON diff --git a/runtime/go/prompty/model/done_event_payload.go b/runtime/go/prompty/model/done_event_payload.go new file mode 100644 index 00000000..89daec80 --- /dev/null +++ b/runtime/go/prompty/model/done_event_payload.go @@ -0,0 +1,98 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// DoneEventPayload represents Payload for "done" events — the agent loop completed successfully. + +type DoneEventPayload struct { + Response string `json:"response" yaml:"response"` + Messages []Message `json:"messages" yaml:"messages"` +} + +// LoadDoneEventPayload creates a DoneEventPayload from a map[string]interface{} +func LoadDoneEventPayload(data interface{}, ctx *LoadContext) (DoneEventPayload, error) { + result := DoneEventPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["response"]; ok && val != nil { + result.Response = val.(string) + } + if val, ok := m["messages"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Messages = make([]Message, len(arr)) + for i, v := range arr { + if item, ok := v.(map[string]interface{}); ok { + loaded, _ := LoadMessage(item, ctx) + result.Messages[i] = loaded + } + } + } + } + } + + return result, nil +} + +// Save serializes DoneEventPayload to map[string]interface{} +func (obj *DoneEventPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["response"] = obj.Response + if obj.Messages != nil { + arr := make([]interface{}, len(obj.Messages)) + for i, item := range obj.Messages { + arr[i] = item.Save(ctx) + } + result["messages"] = arr + } + + return result +} + +// ToJSON serializes DoneEventPayload to JSON string +func (obj *DoneEventPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes DoneEventPayload to YAML string +func (obj *DoneEventPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates DoneEventPayload from JSON string +func DoneEventPayloadFromJSON(jsonStr string) (DoneEventPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return DoneEventPayload{}, err + } + ctx := NewLoadContext() + return LoadDoneEventPayload(data, ctx) +} + +// FromYAML creates DoneEventPayload from YAML string +func DoneEventPayloadFromYAML(yamlStr string) (DoneEventPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return DoneEventPayload{}, err + } + ctx := NewLoadContext() + return LoadDoneEventPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/done_event_payload_test.go b/runtime/go/prompty/model/done_event_payload_test.go new file mode 100644 index 00000000..f621b143 --- /dev/null +++ b/runtime/go/prompty/model/done_event_payload_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestDoneEventPayloadLoadJSON tests loading DoneEventPayload from JSON +func TestDoneEventPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "response": "The weather in Paris is 72°F and sunny." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadDoneEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load DoneEventPayload: %v", err) + } + if instance.Response != "The weather in Paris is 72°F and sunny." { + t.Errorf(`Expected Response to be "The weather in Paris is 72°F and sunny.", got %v`, instance.Response) + } +} + +// TestDoneEventPayloadLoadYAML tests loading DoneEventPayload from YAML +func TestDoneEventPayloadLoadYAML(t *testing.T) { + yamlData := ` +response: The weather in Paris is 72°F and sunny. + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadDoneEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load DoneEventPayload: %v", err) + } + if instance.Response != "The weather in Paris is 72°F and sunny." { + t.Errorf(`Expected Response to be "The weather in Paris is 72°F and sunny.", got %v`, instance.Response) + } +} + +// TestDoneEventPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestDoneEventPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "response": "The weather in Paris is 72°F and sunny." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadDoneEventPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load DoneEventPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadDoneEventPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload DoneEventPayload: %v", err) + } + if reloaded.Response != "The weather in Paris is 72°F and sunny." { + t.Errorf(`Expected Response to be "The weather in Paris is 72°F and sunny.", got %v`, reloaded.Response) + } +} + +// TestDoneEventPayloadToJSON tests that ToJSON produces valid JSON +func TestDoneEventPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "response": "The weather in Paris is 72°F and sunny." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadDoneEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load DoneEventPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestDoneEventPayloadToYAML tests that ToYAML produces valid YAML +func TestDoneEventPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "response": "The weather in Paris is 72°F and sunny." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadDoneEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load DoneEventPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/error_chunk_test.go b/runtime/go/prompty/model/error_chunk_test.go new file mode 100644 index 00000000..2ed29bc4 --- /dev/null +++ b/runtime/go/prompty/model/error_chunk_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestErrorChunkLoadJSON tests loading ErrorChunk from JSON +func TestErrorChunkLoadJSON(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorChunk: %v", err) + } + if instance.Message != "Rate limit exceeded" { + t.Errorf(`Expected Message to be "Rate limit exceeded", got %v`, instance.Message) + } +} + +// TestErrorChunkLoadYAML tests loading ErrorChunk from YAML +func TestErrorChunkLoadYAML(t *testing.T) { + yamlData := ` +message: Rate limit exceeded + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorChunk: %v", err) + } + if instance.Message != "Rate limit exceeded" { + t.Errorf(`Expected Message to be "Rate limit exceeded", got %v`, instance.Message) + } +} + +// TestErrorChunkRoundtrip tests load -> save -> load produces equivalent data +func TestErrorChunkRoundtrip(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorChunk(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ErrorChunk: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadErrorChunk(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ErrorChunk: %v", err) + } + if reloaded.Message != "Rate limit exceeded" { + t.Errorf(`Expected Message to be "Rate limit exceeded", got %v`, reloaded.Message) + } +} + +// TestErrorChunkToJSON tests that ToJSON produces valid JSON +func TestErrorChunkToJSON(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorChunk: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestErrorChunkToYAML tests that ToYAML produces valid YAML +func TestErrorChunkToYAML(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorChunk: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/error_event_payload.go b/runtime/go/prompty/model/error_event_payload.go new file mode 100644 index 00000000..40a7b257 --- /dev/null +++ b/runtime/go/prompty/model/error_event_payload.go @@ -0,0 +1,79 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ErrorEventPayload represents Payload for "error" events — an error occurred during the loop. + +type ErrorEventPayload struct { + Message string `json:"message" yaml:"message"` +} + +// LoadErrorEventPayload creates a ErrorEventPayload from a map[string]interface{} +func LoadErrorEventPayload(data interface{}, ctx *LoadContext) (ErrorEventPayload, error) { + result := ErrorEventPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["message"]; ok && val != nil { + result.Message = val.(string) + } + } + + return result, nil +} + +// Save serializes ErrorEventPayload to map[string]interface{} +func (obj *ErrorEventPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["message"] = obj.Message + + return result +} + +// ToJSON serializes ErrorEventPayload to JSON string +func (obj *ErrorEventPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ErrorEventPayload to YAML string +func (obj *ErrorEventPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ErrorEventPayload from JSON string +func ErrorEventPayloadFromJSON(jsonStr string) (ErrorEventPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ErrorEventPayload{}, err + } + ctx := NewLoadContext() + return LoadErrorEventPayload(data, ctx) +} + +// FromYAML creates ErrorEventPayload from YAML string +func ErrorEventPayloadFromYAML(yamlStr string) (ErrorEventPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ErrorEventPayload{}, err + } + ctx := NewLoadContext() + return LoadErrorEventPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/error_event_payload_test.go b/runtime/go/prompty/model/error_event_payload_test.go new file mode 100644 index 00000000..8054ae7b --- /dev/null +++ b/runtime/go/prompty/model/error_event_payload_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestErrorEventPayloadLoadJSON tests loading ErrorEventPayload from JSON +func TestErrorEventPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorEventPayload: %v", err) + } + if instance.Message != "Rate limit exceeded" { + t.Errorf(`Expected Message to be "Rate limit exceeded", got %v`, instance.Message) + } +} + +// TestErrorEventPayloadLoadYAML tests loading ErrorEventPayload from YAML +func TestErrorEventPayloadLoadYAML(t *testing.T) { + yamlData := ` +message: Rate limit exceeded + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorEventPayload: %v", err) + } + if instance.Message != "Rate limit exceeded" { + t.Errorf(`Expected Message to be "Rate limit exceeded", got %v`, instance.Message) + } +} + +// TestErrorEventPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestErrorEventPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorEventPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ErrorEventPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadErrorEventPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ErrorEventPayload: %v", err) + } + if reloaded.Message != "Rate limit exceeded" { + t.Errorf(`Expected Message to be "Rate limit exceeded", got %v`, reloaded.Message) + } +} + +// TestErrorEventPayloadToJSON tests that ToJSON produces valid JSON +func TestErrorEventPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorEventPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestErrorEventPayloadToYAML tests that ToYAML produces valid YAML +func TestErrorEventPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "message": "Rate limit exceeded" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadErrorEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ErrorEventPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/executor.go b/runtime/go/prompty/model/executor.go new file mode 100644 index 00000000..f67ee678 --- /dev/null +++ b/runtime/go/prompty/model/executor.go @@ -0,0 +1,14 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +// Executor represents Calls an LLM provider with messages and returns the raw provider response. + +type Executor interface { + // Execute — Call an LLM provider with messages and return the raw response + Execute(agent Prompty, messages []Message) (interface{}, error) + // ExecuteStream — Call an LLM provider and return a streaming response. Returns a language-specific async iterable/stream of raw chunks. Not all providers support streaming; the default implementation should signal lack of support. + ExecuteStream(agent Prompty, messages []Message) (interface{}, error) + // FormatToolMessages — Format tool call results into messages for the next iteration + FormatToolMessages(rawResponse interface{}, toolCalls []ToolCall, toolResults []string, textContent *string) ([]Message, error) +} diff --git a/runtime/go/prompty/model/file_part_test.go b/runtime/go/prompty/model/file_part_test.go new file mode 100644 index 00000000..fbcfcea9 --- /dev/null +++ b/runtime/go/prompty/model/file_part_test.go @@ -0,0 +1,154 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestFilePartLoadJSON tests loading FilePart from JSON +func TestFilePartLoadJSON(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadFilePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load FilePart: %v", err) + } + if instance.Source != "https://example.com/document.pdf" { + t.Errorf(`Expected Source to be "https://example.com/document.pdf", got %v`, instance.Source) + } + if instance.MediaType == nil || *instance.MediaType != "application/pdf" { + t.Errorf(`Expected MediaType to be "application/pdf", got %v`, instance.MediaType) + } +} + +// TestFilePartLoadYAML tests loading FilePart from YAML +func TestFilePartLoadYAML(t *testing.T) { + yamlData := ` +source: "https://example.com/document.pdf" +mediaType: application/pdf + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadFilePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load FilePart: %v", err) + } + if instance.Source != "https://example.com/document.pdf" { + t.Errorf(`Expected Source to be "https://example.com/document.pdf", got %v`, instance.Source) + } + if instance.MediaType == nil || *instance.MediaType != "application/pdf" { + t.Errorf(`Expected MediaType to be "application/pdf", got %v`, instance.MediaType) + } +} + +// TestFilePartRoundtrip tests load -> save -> load produces equivalent data +func TestFilePartRoundtrip(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadFilePart(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load FilePart: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadFilePart(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload FilePart: %v", err) + } + if reloaded.Source != "https://example.com/document.pdf" { + t.Errorf(`Expected Source to be "https://example.com/document.pdf", got %v`, reloaded.Source) + } + if reloaded.MediaType == nil || *reloaded.MediaType != "application/pdf" { + t.Errorf(`Expected MediaType to be "application/pdf", got %v`, reloaded.MediaType) + } +} + +// TestFilePartToJSON tests that ToJSON produces valid JSON +func TestFilePartToJSON(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadFilePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load FilePart: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestFilePartToYAML tests that ToYAML produces valid YAML +func TestFilePartToYAML(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadFilePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load FilePart: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/format_config_test.go b/runtime/go/prompty/model/format_config_test.go index df068658..8508deb4 100644 --- a/runtime/go/prompty/model/format_config_test.go +++ b/runtime/go/prompty/model/format_config_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestFormatConfigLoadJSON tests loading FormatConfig from JSON diff --git a/runtime/go/prompty/model/foundry_connection_test.go b/runtime/go/prompty/model/foundry_connection_test.go index 11f8a75b..ec4b88d2 100644 --- a/runtime/go/prompty/model/foundry_connection_test.go +++ b/runtime/go/prompty/model/foundry_connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestFoundryConnectionLoadJSON tests loading FoundryConnection from JSON diff --git a/runtime/go/prompty/model/function_tool_test.go b/runtime/go/prompty/model/function_tool_test.go index 1c86bc7d..56b3b4b7 100644 --- a/runtime/go/prompty/model/function_tool_test.go +++ b/runtime/go/prompty/model/function_tool_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestFunctionToolLoadJSON tests loading FunctionTool from JSON diff --git a/runtime/go/prompty/model/guardrail_result.go b/runtime/go/prompty/model/guardrail_result.go new file mode 100644 index 00000000..1ef7b314 --- /dev/null +++ b/runtime/go/prompty/model/guardrail_result.go @@ -0,0 +1,111 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// GuardrailResult represents The result of a guardrail evaluation. Guardrails are safety checks that +// run at specific phases of the agent loop and can allow, deny, or rewrite +// content. + +type GuardrailResult struct { + Allowed bool `json:"allowed" yaml:"allowed"` + Reason *string `json:"reason,omitempty" yaml:"reason,omitempty"` + Rewrite *interface{} `json:"rewrite,omitempty" yaml:"rewrite,omitempty"` +} + +// LoadGuardrailResult creates a GuardrailResult from a map[string]interface{} +func LoadGuardrailResult(data interface{}, ctx *LoadContext) (GuardrailResult, error) { + result := GuardrailResult{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["allowed"]; ok && val != nil { + result.Allowed = val.(bool) + } + if val, ok := m["reason"]; ok && val != nil { + v := val.(string) + result.Reason = &v + } + if val, ok := m["rewrite"]; ok && val != nil { + result.Rewrite = &val + } + } + + return result, nil +} + +// Save serializes GuardrailResult to map[string]interface{} +func (obj *GuardrailResult) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["allowed"] = obj.Allowed + if obj.Reason != nil { + result["reason"] = *obj.Reason + } + if obj.Rewrite != nil { + result["rewrite"] = *obj.Rewrite + } + + return result +} + +// ToJSON serializes GuardrailResult to JSON string +func (obj *GuardrailResult) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes GuardrailResult to YAML string +func (obj *GuardrailResult) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates GuardrailResult from JSON string +func GuardrailResultFromJSON(jsonStr string) (GuardrailResult, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return GuardrailResult{}, err + } + ctx := NewLoadContext() + return LoadGuardrailResult(data, ctx) +} + +// FromYAML creates GuardrailResult from YAML string +func GuardrailResultFromYAML(yamlStr string) (GuardrailResult, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return GuardrailResult{}, err + } + ctx := NewLoadContext() + return LoadGuardrailResult(data, ctx) +} + +// NewRewriteGuardrailResult creates a GuardrailResult with preset field values. +func NewRewriteGuardrailResult(rewrite interface{}) GuardrailResult { + return GuardrailResult{Allowed: true, Rewrite: ptrOf(rewrite)} +} + +// NewDenyGuardrailResult creates a GuardrailResult with preset field values. +func NewDenyGuardrailResult(reason string) GuardrailResult { + return GuardrailResult{Allowed: false, Reason: ptrOf(reason)} +} + +// NewAllowGuardrailResult creates a GuardrailResult with preset field values. +func NewAllowGuardrailResult() GuardrailResult { + return GuardrailResult{Allowed: true} +} diff --git a/runtime/go/prompty/model/guardrail_result_test.go b/runtime/go/prompty/model/guardrail_result_test.go new file mode 100644 index 00000000..55b29ac8 --- /dev/null +++ b/runtime/go/prompty/model/guardrail_result_test.go @@ -0,0 +1,154 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestGuardrailResultLoadJSON tests loading GuardrailResult from JSON +func TestGuardrailResultLoadJSON(t *testing.T) { + jsonData := ` +{ + "allowed": true, + "reason": "Content is safe" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadGuardrailResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load GuardrailResult: %v", err) + } + if instance.Allowed != true { + t.Errorf(`Expected Allowed to be true, got %v`, instance.Allowed) + } + if instance.Reason == nil || *instance.Reason != "Content is safe" { + t.Errorf(`Expected Reason to be "Content is safe", got %v`, instance.Reason) + } +} + +// TestGuardrailResultLoadYAML tests loading GuardrailResult from YAML +func TestGuardrailResultLoadYAML(t *testing.T) { + yamlData := ` +allowed: true +reason: Content is safe + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadGuardrailResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load GuardrailResult: %v", err) + } + if instance.Allowed != true { + t.Errorf(`Expected Allowed to be true, got %v`, instance.Allowed) + } + if instance.Reason == nil || *instance.Reason != "Content is safe" { + t.Errorf(`Expected Reason to be "Content is safe", got %v`, instance.Reason) + } +} + +// TestGuardrailResultRoundtrip tests load -> save -> load produces equivalent data +func TestGuardrailResultRoundtrip(t *testing.T) { + jsonData := ` +{ + "allowed": true, + "reason": "Content is safe" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadGuardrailResult(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load GuardrailResult: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadGuardrailResult(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload GuardrailResult: %v", err) + } + if reloaded.Allowed != true { + t.Errorf(`Expected Allowed to be true, got %v`, reloaded.Allowed) + } + if reloaded.Reason == nil || *reloaded.Reason != "Content is safe" { + t.Errorf(`Expected Reason to be "Content is safe", got %v`, reloaded.Reason) + } +} + +// TestGuardrailResultToJSON tests that ToJSON produces valid JSON +func TestGuardrailResultToJSON(t *testing.T) { + jsonData := ` +{ + "allowed": true, + "reason": "Content is safe" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadGuardrailResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load GuardrailResult: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestGuardrailResultToYAML tests that ToYAML produces valid YAML +func TestGuardrailResultToYAML(t *testing.T) { + jsonData := ` +{ + "allowed": true, + "reason": "Content is safe" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadGuardrailResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load GuardrailResult: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/helpers.go b/runtime/go/prompty/model/helpers.go new file mode 100644 index 00000000..ab742a16 --- /dev/null +++ b/runtime/go/prompty/model/helpers.go @@ -0,0 +1,33 @@ +// Hand-written helper methods for generated model types. +// Implements the MessageHelpers and ToolResultHelpers interfaces +// declared by the TypeSpec emitter. + +package prompty + +import "strings" + +// Text concatenates all TextPart values in the message, joined by newline. +func (m *Message) Text() string { + var texts []string + for _, part := range m.Parts { + if tp, ok := part.(TextPart); ok { + texts = append(texts, tp.Value) + } else if ptr, ok := part.(*TextPart); ok { + texts = append(texts, ptr.Value) + } + } + return strings.Join(texts, "\n") +} + +// Text concatenates all TextPart values in the tool result, joined by newline. +func (tr *ToolResult) Text() string { + var texts []string + for _, part := range tr.Parts { + if tp, ok := part.(TextPart); ok { + texts = append(texts, tp.Value) + } else if ptr, ok := part.(*TextPart); ok { + texts = append(texts, ptr.Value) + } + } + return strings.Join(texts, "\n") +} diff --git a/runtime/go/prompty/model/image_part_test.go b/runtime/go/prompty/model/image_part_test.go new file mode 100644 index 00000000..f3b104ac --- /dev/null +++ b/runtime/go/prompty/model/image_part_test.go @@ -0,0 +1,168 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestImagePartLoadJSON tests loading ImagePart from JSON +func TestImagePartLoadJSON(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadImagePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load ImagePart: %v", err) + } + if instance.Source != "https://example.com/image.png" { + t.Errorf(`Expected Source to be "https://example.com/image.png", got %v`, instance.Source) + } + if instance.Detail == nil || *instance.Detail != "auto" { + t.Errorf(`Expected Detail to be "auto", got %v`, instance.Detail) + } + if instance.MediaType == nil || *instance.MediaType != "image/png" { + t.Errorf(`Expected MediaType to be "image/png", got %v`, instance.MediaType) + } +} + +// TestImagePartLoadYAML tests loading ImagePart from YAML +func TestImagePartLoadYAML(t *testing.T) { + yamlData := ` +source: "https://example.com/image.png" +detail: auto +mediaType: image/png + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadImagePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load ImagePart: %v", err) + } + if instance.Source != "https://example.com/image.png" { + t.Errorf(`Expected Source to be "https://example.com/image.png", got %v`, instance.Source) + } + if instance.Detail == nil || *instance.Detail != "auto" { + t.Errorf(`Expected Detail to be "auto", got %v`, instance.Detail) + } + if instance.MediaType == nil || *instance.MediaType != "image/png" { + t.Errorf(`Expected MediaType to be "image/png", got %v`, instance.MediaType) + } +} + +// TestImagePartRoundtrip tests load -> save -> load produces equivalent data +func TestImagePartRoundtrip(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadImagePart(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ImagePart: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadImagePart(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ImagePart: %v", err) + } + if reloaded.Source != "https://example.com/image.png" { + t.Errorf(`Expected Source to be "https://example.com/image.png", got %v`, reloaded.Source) + } + if reloaded.Detail == nil || *reloaded.Detail != "auto" { + t.Errorf(`Expected Detail to be "auto", got %v`, reloaded.Detail) + } + if reloaded.MediaType == nil || *reloaded.MediaType != "image/png" { + t.Errorf(`Expected MediaType to be "image/png", got %v`, reloaded.MediaType) + } +} + +// TestImagePartToJSON tests that ToJSON produces valid JSON +func TestImagePartToJSON(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadImagePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load ImagePart: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestImagePartToYAML tests that ToYAML produces valid YAML +func TestImagePartToYAML(t *testing.T) { + jsonData := ` +{ + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadImagePart(data, ctx) + if err != nil { + t.Fatalf("Failed to load ImagePart: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/mcp_approval_mode_test.go b/runtime/go/prompty/model/mcp_approval_mode_test.go index 92131fe4..a81a5f72 100644 --- a/runtime/go/prompty/model/mcp_approval_mode_test.go +++ b/runtime/go/prompty/model/mcp_approval_mode_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestMcpApprovalModeLoadJSON tests loading McpApprovalMode from JSON diff --git a/runtime/go/prompty/model/mcp_tool_test.go b/runtime/go/prompty/model/mcp_tool_test.go index 29667d5f..83b488ff 100644 --- a/runtime/go/prompty/model/mcp_tool_test.go +++ b/runtime/go/prompty/model/mcp_tool_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestMcpToolLoadJSON tests loading McpTool from JSON diff --git a/runtime/go/prompty/model/message.go b/runtime/go/prompty/model/message.go new file mode 100644 index 00000000..4d3f09e5 --- /dev/null +++ b/runtime/go/prompty/model/message.go @@ -0,0 +1,139 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// Message represents A message in a conversation. Messages have a role and a list of content parts +// representing the different modalities of the message content. + +type Message struct { + Role string `json:"role" yaml:"role"` + Parts []interface{} `json:"parts" yaml:"parts"` + Metadata map[string]interface{} `json:"metadata" yaml:"metadata"` +} + +// LoadMessage creates a Message from a map[string]interface{} +func LoadMessage(data interface{}, ctx *LoadContext) (Message, error) { + result := Message{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["role"]; ok && val != nil { + result.Role = val.(string) + } + if val, ok := m["parts"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Parts = make([]interface{}, len(arr)) + for i, v := range arr { + if item, ok := v.(map[string]interface{}); ok { + loaded, _ := LoadContentPart(item, ctx) + // Polymorphic type - store as interface{} + result.Parts[i] = loaded + } + } + } + } + if val, ok := m["metadata"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + result.Metadata = m + } + } + } + + return result, nil +} + +// Save serializes Message to map[string]interface{} +func (obj *Message) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["role"] = obj.Role + if obj.Parts != nil { + arr := make([]interface{}, len(obj.Parts)) + for i, item := range obj.Parts { + // Handle polymorphic type via type switch + switch v := item.(type) { + case interface { + Save(*SaveContext) map[string]interface{} + }: + arr[i] = v.Save(ctx) + default: + arr[i] = item + } + } + result["parts"] = arr + } + result["metadata"] = obj.Metadata + + return result +} + +// ToJSON serializes Message to JSON string +func (obj *Message) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes Message to YAML string +func (obj *Message) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates Message from JSON string +func MessageFromJSON(jsonStr string) (Message, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return Message{}, err + } + ctx := NewLoadContext() + return LoadMessage(data, ctx) +} + +// FromYAML creates Message from YAML string +func MessageFromYAML(yamlStr string) (Message, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return Message{}, err + } + ctx := NewLoadContext() + return LoadMessage(data, ctx) +} + +// NewAssistantMessage creates a Message with preset field values. +func NewAssistantMessage(text string) Message { + return Message{Role: "assistant", Parts: []interface{}{TextPart{Kind: "text", Value: text}}} +} + +// NewSystemMessage creates a Message with preset field values. +func NewSystemMessage(text string) Message { + return Message{Role: "system", Parts: []interface{}{TextPart{Kind: "text", Value: text}}} +} + +// NewUserMessage creates a Message with preset field values. +func NewUserMessage(text string) Message { + return Message{Role: "user", Parts: []interface{}{TextPart{Kind: "text", Value: text}}} +} + +// MessageHelpers defines helper methods for Message. +// Implement these in a separate file (e.g., message_helpers.go). +type MessageHelpers interface { + // ToTextContent — Return plain string if all parts are text, else a list of content part dicts for wire serialization + ToTextContent() interface{} + // Text — Concatenate all TextPart values joined by newline + Text() string +} diff --git a/runtime/go/prompty/model/message_test.go b/runtime/go/prompty/model/message_test.go new file mode 100644 index 00000000..3e22ff39 --- /dev/null +++ b/runtime/go/prompty/model/message_test.go @@ -0,0 +1,181 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestMessageLoadJSON tests loading Message from JSON +func TestMessageLoadJSON(t *testing.T) { + jsonData := ` +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadMessage(data, ctx) + if err != nil { + t.Fatalf("Failed to load Message: %v", err) + } + if instance.Role != "user" { + t.Errorf(`Expected Role to be "user", got %v`, instance.Role) + } +} + +// TestMessageLoadYAML tests loading Message from YAML +func TestMessageLoadYAML(t *testing.T) { + yamlData := ` +role: user +parts: + - kind: text + value: Hello! +metadata: + source: user-input + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadMessage(data, ctx) + if err != nil { + t.Fatalf("Failed to load Message: %v", err) + } + if instance.Role != "user" { + t.Errorf(`Expected Role to be "user", got %v`, instance.Role) + } +} + +// TestMessageRoundtrip tests load -> save -> load produces equivalent data +func TestMessageRoundtrip(t *testing.T) { + jsonData := ` +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadMessage(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load Message: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadMessage(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload Message: %v", err) + } + if reloaded.Role != "user" { + t.Errorf(`Expected Role to be "user", got %v`, reloaded.Role) + } +} + +// TestMessageToJSON tests that ToJSON produces valid JSON +func TestMessageToJSON(t *testing.T) { + jsonData := ` +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadMessage(data, ctx) + if err != nil { + t.Fatalf("Failed to load Message: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestMessageToYAML tests that ToYAML produces valid YAML +func TestMessageToYAML(t *testing.T) { + jsonData := ` +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadMessage(data, ctx) + if err != nil { + t.Fatalf("Failed to load Message: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/messages_updated_payload.go b/runtime/go/prompty/model/messages_updated_payload.go new file mode 100644 index 00000000..85486779 --- /dev/null +++ b/runtime/go/prompty/model/messages_updated_payload.go @@ -0,0 +1,93 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// MessagesUpdatedPayload represents Payload for "messages_updated" events — the conversation state has changed. + +type MessagesUpdatedPayload struct { + Messages []Message `json:"messages" yaml:"messages"` +} + +// LoadMessagesUpdatedPayload creates a MessagesUpdatedPayload from a map[string]interface{} +func LoadMessagesUpdatedPayload(data interface{}, ctx *LoadContext) (MessagesUpdatedPayload, error) { + result := MessagesUpdatedPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["messages"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Messages = make([]Message, len(arr)) + for i, v := range arr { + if item, ok := v.(map[string]interface{}); ok { + loaded, _ := LoadMessage(item, ctx) + result.Messages[i] = loaded + } + } + } + } + } + + return result, nil +} + +// Save serializes MessagesUpdatedPayload to map[string]interface{} +func (obj *MessagesUpdatedPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + if obj.Messages != nil { + arr := make([]interface{}, len(obj.Messages)) + for i, item := range obj.Messages { + arr[i] = item.Save(ctx) + } + result["messages"] = arr + } + + return result +} + +// ToJSON serializes MessagesUpdatedPayload to JSON string +func (obj *MessagesUpdatedPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes MessagesUpdatedPayload to YAML string +func (obj *MessagesUpdatedPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates MessagesUpdatedPayload from JSON string +func MessagesUpdatedPayloadFromJSON(jsonStr string) (MessagesUpdatedPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return MessagesUpdatedPayload{}, err + } + ctx := NewLoadContext() + return LoadMessagesUpdatedPayload(data, ctx) +} + +// FromYAML creates MessagesUpdatedPayload from YAML string +func MessagesUpdatedPayloadFromYAML(yamlStr string) (MessagesUpdatedPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return MessagesUpdatedPayload{}, err + } + ctx := NewLoadContext() + return LoadMessagesUpdatedPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/messages_updated_payload_test.go b/runtime/go/prompty/model/messages_updated_payload_test.go new file mode 100644 index 00000000..8e719bf5 --- /dev/null +++ b/runtime/go/prompty/model/messages_updated_payload_test.go @@ -0,0 +1,3 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test diff --git a/runtime/go/prompty/model/model_info.go b/runtime/go/prompty/model/model_info.go new file mode 100644 index 00000000..917a4990 --- /dev/null +++ b/runtime/go/prompty/model/model_info.go @@ -0,0 +1,147 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ModelInfo represents Information about a model available from a provider. Used by provider-level +// model discovery to report which models are available and their capabilities. +// +// Not all providers return all fields — implementations SHOULD populate as +// many fields as the provider's API supports and MAY enrich sparse results +// from a built-in lookup table of known models. + +type ModelInfo struct { + Id string `json:"id" yaml:"id"` + DisplayName *string `json:"displayName,omitempty" yaml:"displayName,omitempty"` + OwnedBy *string `json:"ownedBy,omitempty" yaml:"ownedBy,omitempty"` + ContextWindow *int32 `json:"contextWindow,omitempty" yaml:"contextWindow,omitempty"` + InputModalities []string `json:"inputModalities,omitempty" yaml:"inputModalities,omitempty"` + OutputModalities []string `json:"outputModalities,omitempty" yaml:"outputModalities,omitempty"` + AdditionalProperties map[string]interface{} `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` +} + +// LoadModelInfo creates a ModelInfo from a map[string]interface{} +func LoadModelInfo(data interface{}, ctx *LoadContext) (ModelInfo, error) { + result := ModelInfo{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["id"]; ok && val != nil { + result.Id = val.(string) + } + if val, ok := m["displayName"]; ok && val != nil { + v := val.(string) + result.DisplayName = &v + } + if val, ok := m["ownedBy"]; ok && val != nil { + v := val.(string) + result.OwnedBy = &v + } + if val, ok := m["contextWindow"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.ContextWindow = &v + } + if val, ok := m["inputModalities"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.InputModalities = make([]string, len(arr)) + for i, v := range arr { + result.InputModalities[i] = v.(string) + } + } + } + if val, ok := m["outputModalities"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.OutputModalities = make([]string, len(arr)) + for i, v := range arr { + result.OutputModalities[i] = v.(string) + } + } + } + if val, ok := m["additionalProperties"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + result.AdditionalProperties = m + } + } + } + + return result, nil +} + +// Save serializes ModelInfo to map[string]interface{} +func (obj *ModelInfo) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["id"] = obj.Id + if obj.DisplayName != nil { + result["displayName"] = *obj.DisplayName + } + if obj.OwnedBy != nil { + result["ownedBy"] = *obj.OwnedBy + } + if obj.ContextWindow != nil { + result["contextWindow"] = *obj.ContextWindow + } + result["inputModalities"] = obj.InputModalities + result["outputModalities"] = obj.OutputModalities + if obj.AdditionalProperties != nil { + result["additionalProperties"] = obj.AdditionalProperties + } + + return result +} + +// ToJSON serializes ModelInfo to JSON string +func (obj *ModelInfo) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ModelInfo to YAML string +func (obj *ModelInfo) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ModelInfo from JSON string +func ModelInfoFromJSON(jsonStr string) (ModelInfo, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ModelInfo{}, err + } + ctx := NewLoadContext() + return LoadModelInfo(data, ctx) +} + +// FromYAML creates ModelInfo from YAML string +func ModelInfoFromYAML(yamlStr string) (ModelInfo, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ModelInfo{}, err + } + ctx := NewLoadContext() + return LoadModelInfo(data, ctx) +} diff --git a/runtime/go/prompty/model/model_info_test.go b/runtime/go/prompty/model/model_info_test.go new file mode 100644 index 00000000..5bebaf6a --- /dev/null +++ b/runtime/go/prompty/model/model_info_test.go @@ -0,0 +1,229 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestModelInfoLoadJSON tests loading ModelInfo from JSON +func TestModelInfoLoadJSON(t *testing.T) { + jsonData := ` +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadModelInfo(data, ctx) + if err != nil { + t.Fatalf("Failed to load ModelInfo: %v", err) + } + if instance.Id != "gpt-4o" { + t.Errorf(`Expected Id to be "gpt-4o", got %v`, instance.Id) + } + if instance.DisplayName == nil || *instance.DisplayName != "GPT-4o" { + t.Errorf(`Expected DisplayName to be "GPT-4o", got %v`, instance.DisplayName) + } + if instance.OwnedBy == nil || *instance.OwnedBy != "openai" { + t.Errorf(`Expected OwnedBy to be "openai", got %v`, instance.OwnedBy) + } + if instance.ContextWindow == nil || *instance.ContextWindow != 128000 { + t.Errorf(`Expected ContextWindow to be 128000, got %v`, instance.ContextWindow) + } +} + +// TestModelInfoLoadYAML tests loading ModelInfo from YAML +func TestModelInfoLoadYAML(t *testing.T) { + yamlData := ` +id: gpt-4o +displayName: GPT-4o +ownedBy: openai +contextWindow: 128000 +inputModalities: + - text + - image +outputModalities: + - text +additionalProperties: + supportsStreaming: true + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadModelInfo(data, ctx) + if err != nil { + t.Fatalf("Failed to load ModelInfo: %v", err) + } + if instance.Id != "gpt-4o" { + t.Errorf(`Expected Id to be "gpt-4o", got %v`, instance.Id) + } + if instance.DisplayName == nil || *instance.DisplayName != "GPT-4o" { + t.Errorf(`Expected DisplayName to be "GPT-4o", got %v`, instance.DisplayName) + } + if instance.OwnedBy == nil || *instance.OwnedBy != "openai" { + t.Errorf(`Expected OwnedBy to be "openai", got %v`, instance.OwnedBy) + } + if instance.ContextWindow == nil || *instance.ContextWindow != 128000 { + t.Errorf(`Expected ContextWindow to be 128000, got %v`, instance.ContextWindow) + } +} + +// TestModelInfoRoundtrip tests load -> save -> load produces equivalent data +func TestModelInfoRoundtrip(t *testing.T) { + jsonData := ` +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadModelInfo(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ModelInfo: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadModelInfo(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ModelInfo: %v", err) + } + if reloaded.Id != "gpt-4o" { + t.Errorf(`Expected Id to be "gpt-4o", got %v`, reloaded.Id) + } + if reloaded.DisplayName == nil || *reloaded.DisplayName != "GPT-4o" { + t.Errorf(`Expected DisplayName to be "GPT-4o", got %v`, reloaded.DisplayName) + } + if reloaded.OwnedBy == nil || *reloaded.OwnedBy != "openai" { + t.Errorf(`Expected OwnedBy to be "openai", got %v`, reloaded.OwnedBy) + } + if reloaded.ContextWindow == nil || *reloaded.ContextWindow != 128000 { + t.Errorf(`Expected ContextWindow to be 128000, got %v`, reloaded.ContextWindow) + } +} + +// TestModelInfoToJSON tests that ToJSON produces valid JSON +func TestModelInfoToJSON(t *testing.T) { + jsonData := ` +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadModelInfo(data, ctx) + if err != nil { + t.Fatalf("Failed to load ModelInfo: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestModelInfoToYAML tests that ToYAML produces valid YAML +func TestModelInfoToYAML(t *testing.T) { + jsonData := ` +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadModelInfo(data, ctx) + if err != nil { + t.Fatalf("Failed to load ModelInfo: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/model_options.go b/runtime/go/prompty/model/model_options.go index 643f7b99..1a8bef2b 100644 --- a/runtime/go/prompty/model/model_options.go +++ b/runtime/go/prompty/model/model_options.go @@ -192,6 +192,31 @@ func (obj *ModelOptions) Save(ctx *SaveContext) map[string]interface{} { return result } +// ToWire converts to provider-specific wire format. +func (obj *ModelOptions) ToWire(provider string) map[string]interface{} { + data := obj.Save(nil) + result := make(map[string]interface{}) + wireMap := map[string]map[string]string{ + "frequencyPenalty": {"openai": "frequency_penalty"}, + "maxOutputTokens": {"openai": "max_completion_tokens", "responses": "max_output_tokens", "anthropic": "max_tokens"}, + "presencePenalty": {"openai": "presence_penalty"}, + "seed": {"openai": "seed"}, + "temperature": {"openai": "temperature", "responses": "temperature", "anthropic": "temperature"}, + "topK": {"openai": "top_k", "anthropic": "top_k"}, + "topP": {"openai": "top_p", "responses": "top_p", "anthropic": "top_p"}, + "stopSequences": {"openai": "stop", "anthropic": "stop_sequences"}, + "allowMultipleToolCalls": {"openai": "parallel_tool_calls"}, + } + for key, value := range data { + if mapping, ok := wireMap[key]; ok { + if wireName, ok := mapping[provider]; ok { + result[wireName] = value + } + } + } + return result +} + // ToJSON serializes ModelOptions to JSON string func (obj *ModelOptions) ToJSON() (string, error) { ctx := NewSaveContext() diff --git a/runtime/go/prompty/model/model_options_test.go b/runtime/go/prompty/model/model_options_test.go index e21925ef..dc698a3b 100644 --- a/runtime/go/prompty/model/model_options_test.go +++ b/runtime/go/prompty/model/model_options_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestModelOptionsLoadJSON tests loading ModelOptions from JSON diff --git a/runtime/go/prompty/model/model_test.go b/runtime/go/prompty/model/model_test.go index 7182e635..e27d2c57 100644 --- a/runtime/go/prompty/model/model_test.go +++ b/runtime/go/prompty/model/model_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestModelLoadJSON tests loading Model from JSON diff --git a/runtime/go/prompty/model/o_auth_connection_test.go b/runtime/go/prompty/model/o_auth_connection_test.go index 64dee97d..f6d94011 100644 --- a/runtime/go/prompty/model/o_auth_connection_test.go +++ b/runtime/go/prompty/model/o_auth_connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestOAuthConnectionLoadJSON tests loading OAuthConnection from JSON diff --git a/runtime/go/prompty/model/object_property_test.go b/runtime/go/prompty/model/object_property_test.go index 294e78c0..e13b53f8 100644 --- a/runtime/go/prompty/model/object_property_test.go +++ b/runtime/go/prompty/model/object_property_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestObjectPropertyLoadJSON tests loading ObjectProperty from JSON diff --git a/runtime/go/prompty/model/open_api_tool_test.go b/runtime/go/prompty/model/open_api_tool_test.go index 2322b4e1..4616269e 100644 --- a/runtime/go/prompty/model/open_api_tool_test.go +++ b/runtime/go/prompty/model/open_api_tool_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestOpenApiToolLoadJSON tests loading OpenApiTool from JSON diff --git a/runtime/go/prompty/model/parser.go b/runtime/go/prompty/model/parser.go new file mode 100644 index 00000000..5ab22509 --- /dev/null +++ b/runtime/go/prompty/model/parser.go @@ -0,0 +1,12 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +// Parser represents Parses rendered prompt text into an array of structured messages with role markers. + +type Parser interface { + // PreRender — Pre-process a template before rendering, returning modified template and context + PreRender(template string) *interface{} + // Parse — Parse rendered text into a structured message array + Parse(agent Prompty, rendered string, context *map[string]interface{}) ([]Message, error) +} diff --git a/runtime/go/prompty/model/parser_config_test.go b/runtime/go/prompty/model/parser_config_test.go index 59c10747..b2c053db 100644 --- a/runtime/go/prompty/model/parser_config_test.go +++ b/runtime/go/prompty/model/parser_config_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestParserConfigLoadJSON tests loading ParserConfig from JSON diff --git a/runtime/go/prompty/model/processor.go b/runtime/go/prompty/model/processor.go new file mode 100644 index 00000000..39f431f4 --- /dev/null +++ b/runtime/go/prompty/model/processor.go @@ -0,0 +1,12 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +// Processor represents Extracts a clean, typed result from a raw LLM provider response. + +type Processor interface { + // Process — Extract a clean result from a raw LLM response + Process(agent Prompty, response interface{}) (interface{}, error) + // ProcessStream — Process a streaming response into a stream of StreamChunk items. Takes raw chunks from the executor and yields processed text, thinking, tool, or error chunks. Not all providers support streaming; the default implementation should signal lack of support. + ProcessStream(stream interface{}) (interface{}, error) +} diff --git a/runtime/go/prompty/model/prompty_test.go b/runtime/go/prompty/model/prompty_test.go index 7f4d2260..40a0aa56 100644 --- a/runtime/go/prompty/model/prompty_test.go +++ b/runtime/go/prompty/model/prompty_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestPromptyLoadJSON tests loading Prompty from JSON diff --git a/runtime/go/prompty/model/prompty_tool_test.go b/runtime/go/prompty/model/prompty_tool_test.go index 880a7efe..039cee1b 100644 --- a/runtime/go/prompty/model/prompty_tool_test.go +++ b/runtime/go/prompty/model/prompty_tool_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestPromptyToolLoadJSON tests loading PromptyTool from JSON diff --git a/runtime/go/prompty/model/property_test.go b/runtime/go/prompty/model/property_test.go index a943f8f1..c7871a7e 100644 --- a/runtime/go/prompty/model/property_test.go +++ b/runtime/go/prompty/model/property_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestPropertyLoadJSON tests loading Property from JSON diff --git a/runtime/go/prompty/model/reference_connection_test.go b/runtime/go/prompty/model/reference_connection_test.go index 7914fbc1..2a798c4a 100644 --- a/runtime/go/prompty/model/reference_connection_test.go +++ b/runtime/go/prompty/model/reference_connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestReferenceConnectionLoadJSON tests loading ReferenceConnection from JSON diff --git a/runtime/go/prompty/model/remote_connection_test.go b/runtime/go/prompty/model/remote_connection_test.go index 77e598a4..2599b621 100644 --- a/runtime/go/prompty/model/remote_connection_test.go +++ b/runtime/go/prompty/model/remote_connection_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestRemoteConnectionLoadJSON tests loading RemoteConnection from JSON diff --git a/runtime/go/prompty/model/renderer.go b/runtime/go/prompty/model/renderer.go new file mode 100644 index 00000000..e1e5c268 --- /dev/null +++ b/runtime/go/prompty/model/renderer.go @@ -0,0 +1,10 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +// Renderer represents Renders a template string with input values to produce the final prompt text. + +type Renderer interface { + // Render — Render the template string with input values + Render(agent Prompty, template string, inputs map[string]interface{}) (string, error) +} diff --git a/runtime/go/prompty/model/status_event_payload.go b/runtime/go/prompty/model/status_event_payload.go new file mode 100644 index 00000000..985181e3 --- /dev/null +++ b/runtime/go/prompty/model/status_event_payload.go @@ -0,0 +1,79 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// StatusEventPayload represents Payload for "status" events — informational messages about loop progress. + +type StatusEventPayload struct { + Message string `json:"message" yaml:"message"` +} + +// LoadStatusEventPayload creates a StatusEventPayload from a map[string]interface{} +func LoadStatusEventPayload(data interface{}, ctx *LoadContext) (StatusEventPayload, error) { + result := StatusEventPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["message"]; ok && val != nil { + result.Message = val.(string) + } + } + + return result, nil +} + +// Save serializes StatusEventPayload to map[string]interface{} +func (obj *StatusEventPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["message"] = obj.Message + + return result +} + +// ToJSON serializes StatusEventPayload to JSON string +func (obj *StatusEventPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes StatusEventPayload to YAML string +func (obj *StatusEventPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates StatusEventPayload from JSON string +func StatusEventPayloadFromJSON(jsonStr string) (StatusEventPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return StatusEventPayload{}, err + } + ctx := NewLoadContext() + return LoadStatusEventPayload(data, ctx) +} + +// FromYAML creates StatusEventPayload from YAML string +func StatusEventPayloadFromYAML(yamlStr string) (StatusEventPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return StatusEventPayload{}, err + } + ctx := NewLoadContext() + return LoadStatusEventPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/status_event_payload_test.go b/runtime/go/prompty/model/status_event_payload_test.go new file mode 100644 index 00000000..41c8950a --- /dev/null +++ b/runtime/go/prompty/model/status_event_payload_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestStatusEventPayloadLoadJSON tests loading StatusEventPayload from JSON +func TestStatusEventPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "message": "Starting iteration 3" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadStatusEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load StatusEventPayload: %v", err) + } + if instance.Message != "Starting iteration 3" { + t.Errorf(`Expected Message to be "Starting iteration 3", got %v`, instance.Message) + } +} + +// TestStatusEventPayloadLoadYAML tests loading StatusEventPayload from YAML +func TestStatusEventPayloadLoadYAML(t *testing.T) { + yamlData := ` +message: Starting iteration 3 + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadStatusEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load StatusEventPayload: %v", err) + } + if instance.Message != "Starting iteration 3" { + t.Errorf(`Expected Message to be "Starting iteration 3", got %v`, instance.Message) + } +} + +// TestStatusEventPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestStatusEventPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "message": "Starting iteration 3" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadStatusEventPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load StatusEventPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadStatusEventPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload StatusEventPayload: %v", err) + } + if reloaded.Message != "Starting iteration 3" { + t.Errorf(`Expected Message to be "Starting iteration 3", got %v`, reloaded.Message) + } +} + +// TestStatusEventPayloadToJSON tests that ToJSON produces valid JSON +func TestStatusEventPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "message": "Starting iteration 3" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadStatusEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load StatusEventPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestStatusEventPayloadToYAML tests that ToYAML produces valid YAML +func TestStatusEventPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "message": "Starting iteration 3" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadStatusEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load StatusEventPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/stream_chunk.go b/runtime/go/prompty/model/stream_chunk.go new file mode 100644 index 00000000..a881c2c4 --- /dev/null +++ b/runtime/go/prompty/model/stream_chunk.go @@ -0,0 +1,402 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// StreamChunk represents A chunk of data from a streaming LLM response. Stream chunks are +// discriminated on the `kind` field. + +type StreamChunk struct { + Kind string `json:"kind" yaml:"kind"` +} + +// LoadStreamChunk creates a StreamChunk from a map[string]interface{} +// Returns interface{} because this is a polymorphic base type that can resolve to different child types +func LoadStreamChunk(data interface{}, ctx *LoadContext) (interface{}, error) { + result := StreamChunk{} + + // Handle polymorphic types based on discriminator + if m, ok := data.(map[string]interface{}); ok { + if discriminator, ok := m["kind"]; ok { + switch discriminator { + case "text": + return LoadTextChunk(data, ctx) + case "thinking": + return LoadThinkingChunk(data, ctx) + case "tool": + return LoadToolChunk(data, ctx) + case "error": + return LoadErrorChunk(data, ctx) + } + } + } + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + } + + return result, nil +} + +// Save serializes StreamChunk to map[string]interface{} +func (obj *StreamChunk) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + + return result +} + +// ToJSON serializes StreamChunk to JSON string +func (obj *StreamChunk) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes StreamChunk to YAML string +func (obj *StreamChunk) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates StreamChunk from JSON string +// Returns interface{} because this is a polymorphic base type that can resolve to different child types +func StreamChunkFromJSON(jsonStr string) (interface{}, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return nil, err + } + ctx := NewLoadContext() + return LoadStreamChunk(data, ctx) +} + +// FromYAML creates StreamChunk from YAML string +// Returns interface{} because this is a polymorphic base type that can resolve to different child types +func StreamChunkFromYAML(yamlStr string) (interface{}, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return nil, err + } + ctx := NewLoadContext() + return LoadStreamChunk(data, ctx) +} + +// TextChunk represents A text content chunk from the LLM response stream. + +type TextChunk struct { + Kind string `json:"kind" yaml:"kind"` + Value string `json:"value" yaml:"value"` +} + +// LoadTextChunk creates a TextChunk from a map[string]interface{} +func LoadTextChunk(data interface{}, ctx *LoadContext) (TextChunk, error) { + result := TextChunk{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["value"]; ok && val != nil { + result.Value = val.(string) + } + } + + return result, nil +} + +// Save serializes TextChunk to map[string]interface{} +func (obj *TextChunk) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["value"] = obj.Value + + return result +} + +// ToJSON serializes TextChunk to JSON string +func (obj *TextChunk) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes TextChunk to YAML string +func (obj *TextChunk) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates TextChunk from JSON string +func TextChunkFromJSON(jsonStr string) (TextChunk, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return TextChunk{}, err + } + ctx := NewLoadContext() + return LoadTextChunk(data, ctx) +} + +// FromYAML creates TextChunk from YAML string +func TextChunkFromYAML(yamlStr string) (TextChunk, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return TextChunk{}, err + } + ctx := NewLoadContext() + return LoadTextChunk(data, ctx) +} + +// ThinkingChunk represents A thinking/reasoning content chunk from the LLM response stream. + +type ThinkingChunk struct { + Kind string `json:"kind" yaml:"kind"` + Value string `json:"value" yaml:"value"` +} + +// LoadThinkingChunk creates a ThinkingChunk from a map[string]interface{} +func LoadThinkingChunk(data interface{}, ctx *LoadContext) (ThinkingChunk, error) { + result := ThinkingChunk{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["value"]; ok && val != nil { + result.Value = val.(string) + } + } + + return result, nil +} + +// Save serializes ThinkingChunk to map[string]interface{} +func (obj *ThinkingChunk) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["value"] = obj.Value + + return result +} + +// ToJSON serializes ThinkingChunk to JSON string +func (obj *ThinkingChunk) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ThinkingChunk to YAML string +func (obj *ThinkingChunk) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ThinkingChunk from JSON string +func ThinkingChunkFromJSON(jsonStr string) (ThinkingChunk, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ThinkingChunk{}, err + } + ctx := NewLoadContext() + return LoadThinkingChunk(data, ctx) +} + +// FromYAML creates ThinkingChunk from YAML string +func ThinkingChunkFromYAML(yamlStr string) (ThinkingChunk, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ThinkingChunk{}, err + } + ctx := NewLoadContext() + return LoadThinkingChunk(data, ctx) +} + +// ToolChunk represents A tool call chunk from the LLM response stream. + +type ToolChunk struct { + Kind string `json:"kind" yaml:"kind"` + ToolCall ToolCall `json:"toolCall" yaml:"toolCall"` +} + +// LoadToolChunk creates a ToolChunk from a map[string]interface{} +func LoadToolChunk(data interface{}, ctx *LoadContext) (ToolChunk, error) { + result := ToolChunk{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["toolCall"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + loaded, _ := LoadToolCall(m, ctx) + result.ToolCall = loaded + } + } + } + + return result, nil +} + +// Save serializes ToolChunk to map[string]interface{} +func (obj *ToolChunk) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + + result["toolCall"] = obj.ToolCall.Save(ctx) + + return result +} + +// ToJSON serializes ToolChunk to JSON string +func (obj *ToolChunk) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolChunk to YAML string +func (obj *ToolChunk) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolChunk from JSON string +func ToolChunkFromJSON(jsonStr string) (ToolChunk, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolChunk{}, err + } + ctx := NewLoadContext() + return LoadToolChunk(data, ctx) +} + +// FromYAML creates ToolChunk from YAML string +func ToolChunkFromYAML(yamlStr string) (ToolChunk, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolChunk{}, err + } + ctx := NewLoadContext() + return LoadToolChunk(data, ctx) +} + +// ErrorChunk represents An error chunk from the LLM response stream. + +type ErrorChunk struct { + Kind string `json:"kind" yaml:"kind"` + Message string `json:"message" yaml:"message"` +} + +// LoadErrorChunk creates a ErrorChunk from a map[string]interface{} +func LoadErrorChunk(data interface{}, ctx *LoadContext) (ErrorChunk, error) { + result := ErrorChunk{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["message"]; ok && val != nil { + result.Message = val.(string) + } + } + + return result, nil +} + +// Save serializes ErrorChunk to map[string]interface{} +func (obj *ErrorChunk) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + result["message"] = obj.Message + + return result +} + +// ToJSON serializes ErrorChunk to JSON string +func (obj *ErrorChunk) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ErrorChunk to YAML string +func (obj *ErrorChunk) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ErrorChunk from JSON string +func ErrorChunkFromJSON(jsonStr string) (ErrorChunk, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ErrorChunk{}, err + } + ctx := NewLoadContext() + return LoadErrorChunk(data, ctx) +} + +// FromYAML creates ErrorChunk from YAML string +func ErrorChunkFromYAML(yamlStr string) (ErrorChunk, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ErrorChunk{}, err + } + ctx := NewLoadContext() + return LoadErrorChunk(data, ctx) +} diff --git a/runtime/go/prompty/model/stream_chunk_test.go b/runtime/go/prompty/model/stream_chunk_test.go new file mode 100644 index 00000000..8e719bf5 --- /dev/null +++ b/runtime/go/prompty/model/stream_chunk_test.go @@ -0,0 +1,3 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test diff --git a/runtime/go/prompty/model/template_test.go b/runtime/go/prompty/model/template_test.go index ff9f895a..7bd91083 100644 --- a/runtime/go/prompty/model/template_test.go +++ b/runtime/go/prompty/model/template_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestTemplateLoadJSON tests loading Template from JSON diff --git a/runtime/go/prompty/model/text_chunk_test.go b/runtime/go/prompty/model/text_chunk_test.go new file mode 100644 index 00000000..dec38867 --- /dev/null +++ b/runtime/go/prompty/model/text_chunk_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestTextChunkLoadJSON tests loading TextChunk from JSON +func TestTextChunkLoadJSON(t *testing.T) { + jsonData := ` +{ + "value": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextChunk: %v", err) + } + if instance.Value != "Hello" { + t.Errorf(`Expected Value to be "Hello", got %v`, instance.Value) + } +} + +// TestTextChunkLoadYAML tests loading TextChunk from YAML +func TestTextChunkLoadYAML(t *testing.T) { + yamlData := ` +value: Hello + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextChunk: %v", err) + } + if instance.Value != "Hello" { + t.Errorf(`Expected Value to be "Hello", got %v`, instance.Value) + } +} + +// TestTextChunkRoundtrip tests load -> save -> load produces equivalent data +func TestTextChunkRoundtrip(t *testing.T) { + jsonData := ` +{ + "value": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadTextChunk(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load TextChunk: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadTextChunk(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload TextChunk: %v", err) + } + if reloaded.Value != "Hello" { + t.Errorf(`Expected Value to be "Hello", got %v`, reloaded.Value) + } +} + +// TestTextChunkToJSON tests that ToJSON produces valid JSON +func TestTextChunkToJSON(t *testing.T) { + jsonData := ` +{ + "value": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextChunk: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestTextChunkToYAML tests that ToYAML produces valid YAML +func TestTextChunkToYAML(t *testing.T) { + jsonData := ` +{ + "value": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextChunk: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/text_part_test.go b/runtime/go/prompty/model/text_part_test.go new file mode 100644 index 00000000..6b6dad2e --- /dev/null +++ b/runtime/go/prompty/model/text_part_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestTextPartLoadJSON tests loading TextPart from JSON +func TestTextPartLoadJSON(t *testing.T) { + jsonData := ` +{ + "value": "Hello, world!" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextPart: %v", err) + } + if instance.Value != "Hello, world!" { + t.Errorf(`Expected Value to be "Hello, world!", got %v`, instance.Value) + } +} + +// TestTextPartLoadYAML tests loading TextPart from YAML +func TestTextPartLoadYAML(t *testing.T) { + yamlData := ` +value: Hello, world! + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextPart: %v", err) + } + if instance.Value != "Hello, world!" { + t.Errorf(`Expected Value to be "Hello, world!", got %v`, instance.Value) + } +} + +// TestTextPartRoundtrip tests load -> save -> load produces equivalent data +func TestTextPartRoundtrip(t *testing.T) { + jsonData := ` +{ + "value": "Hello, world!" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadTextPart(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load TextPart: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadTextPart(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload TextPart: %v", err) + } + if reloaded.Value != "Hello, world!" { + t.Errorf(`Expected Value to be "Hello, world!", got %v`, reloaded.Value) + } +} + +// TestTextPartToJSON tests that ToJSON produces valid JSON +func TestTextPartToJSON(t *testing.T) { + jsonData := ` +{ + "value": "Hello, world!" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextPart: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestTextPartToYAML tests that ToYAML produces valid YAML +func TestTextPartToYAML(t *testing.T) { + jsonData := ` +{ + "value": "Hello, world!" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTextPart(data, ctx) + if err != nil { + t.Fatalf("Failed to load TextPart: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/thinking_chunk_test.go b/runtime/go/prompty/model/thinking_chunk_test.go new file mode 100644 index 00000000..4a5b4754 --- /dev/null +++ b/runtime/go/prompty/model/thinking_chunk_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestThinkingChunkLoadJSON tests loading ThinkingChunk from JSON +func TestThinkingChunkLoadJSON(t *testing.T) { + jsonData := ` +{ + "value": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingChunk: %v", err) + } + if instance.Value != "Let me consider..." { + t.Errorf(`Expected Value to be "Let me consider...", got %v`, instance.Value) + } +} + +// TestThinkingChunkLoadYAML tests loading ThinkingChunk from YAML +func TestThinkingChunkLoadYAML(t *testing.T) { + yamlData := ` +value: Let me consider... + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingChunk: %v", err) + } + if instance.Value != "Let me consider..." { + t.Errorf(`Expected Value to be "Let me consider...", got %v`, instance.Value) + } +} + +// TestThinkingChunkRoundtrip tests load -> save -> load produces equivalent data +func TestThinkingChunkRoundtrip(t *testing.T) { + jsonData := ` +{ + "value": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingChunk(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ThinkingChunk: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadThinkingChunk(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ThinkingChunk: %v", err) + } + if reloaded.Value != "Let me consider..." { + t.Errorf(`Expected Value to be "Let me consider...", got %v`, reloaded.Value) + } +} + +// TestThinkingChunkToJSON tests that ToJSON produces valid JSON +func TestThinkingChunkToJSON(t *testing.T) { + jsonData := ` +{ + "value": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingChunk: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestThinkingChunkToYAML tests that ToYAML produces valid YAML +func TestThinkingChunkToYAML(t *testing.T) { + jsonData := ` +{ + "value": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingChunk: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/thinking_event_payload.go b/runtime/go/prompty/model/thinking_event_payload.go new file mode 100644 index 00000000..35745e75 --- /dev/null +++ b/runtime/go/prompty/model/thinking_event_payload.go @@ -0,0 +1,79 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ThinkingEventPayload represents Payload for "thinking" events — reasoning/chain-of-thought tokens. + +type ThinkingEventPayload struct { + Token string `json:"token" yaml:"token"` +} + +// LoadThinkingEventPayload creates a ThinkingEventPayload from a map[string]interface{} +func LoadThinkingEventPayload(data interface{}, ctx *LoadContext) (ThinkingEventPayload, error) { + result := ThinkingEventPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["token"]; ok && val != nil { + result.Token = val.(string) + } + } + + return result, nil +} + +// Save serializes ThinkingEventPayload to map[string]interface{} +func (obj *ThinkingEventPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["token"] = obj.Token + + return result +} + +// ToJSON serializes ThinkingEventPayload to JSON string +func (obj *ThinkingEventPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ThinkingEventPayload to YAML string +func (obj *ThinkingEventPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ThinkingEventPayload from JSON string +func ThinkingEventPayloadFromJSON(jsonStr string) (ThinkingEventPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ThinkingEventPayload{}, err + } + ctx := NewLoadContext() + return LoadThinkingEventPayload(data, ctx) +} + +// FromYAML creates ThinkingEventPayload from YAML string +func ThinkingEventPayloadFromYAML(yamlStr string) (ThinkingEventPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ThinkingEventPayload{}, err + } + ctx := NewLoadContext() + return LoadThinkingEventPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/thinking_event_payload_test.go b/runtime/go/prompty/model/thinking_event_payload_test.go new file mode 100644 index 00000000..68657314 --- /dev/null +++ b/runtime/go/prompty/model/thinking_event_payload_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestThinkingEventPayloadLoadJSON tests loading ThinkingEventPayload from JSON +func TestThinkingEventPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "token": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingEventPayload: %v", err) + } + if instance.Token != "Let me consider..." { + t.Errorf(`Expected Token to be "Let me consider...", got %v`, instance.Token) + } +} + +// TestThinkingEventPayloadLoadYAML tests loading ThinkingEventPayload from YAML +func TestThinkingEventPayloadLoadYAML(t *testing.T) { + yamlData := ` +token: Let me consider... + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingEventPayload: %v", err) + } + if instance.Token != "Let me consider..." { + t.Errorf(`Expected Token to be "Let me consider...", got %v`, instance.Token) + } +} + +// TestThinkingEventPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestThinkingEventPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "token": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingEventPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ThinkingEventPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadThinkingEventPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ThinkingEventPayload: %v", err) + } + if reloaded.Token != "Let me consider..." { + t.Errorf(`Expected Token to be "Let me consider...", got %v`, reloaded.Token) + } +} + +// TestThinkingEventPayloadToJSON tests that ToJSON produces valid JSON +func TestThinkingEventPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "token": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingEventPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestThinkingEventPayloadToYAML tests that ToYAML produces valid YAML +func TestThinkingEventPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "token": "Let me consider..." +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThinkingEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThinkingEventPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/thread_marker.go b/runtime/go/prompty/model/thread_marker.go new file mode 100644 index 00000000..f49fdc02 --- /dev/null +++ b/runtime/go/prompty/model/thread_marker.go @@ -0,0 +1,88 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ThreadMarker represents Positional marker for conversation history insertion during template rendering. +// +// During `prepare()`, nonce strings in rendered text are replaced with +// ThreadMarker objects. The pipeline then replaces them with actual +// conversation messages from the inputs. + +type ThreadMarker struct { + Name string `json:"name" yaml:"name"` + Kind string `json:"kind" yaml:"kind"` +} + +// LoadThreadMarker creates a ThreadMarker from a map[string]interface{} +func LoadThreadMarker(data interface{}, ctx *LoadContext) (ThreadMarker, error) { + result := ThreadMarker{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["name"]; ok && val != nil { + result.Name = val.(string) + } + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + } + + return result, nil +} + +// Save serializes ThreadMarker to map[string]interface{} +func (obj *ThreadMarker) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["name"] = obj.Name + result["kind"] = obj.Kind + + return result +} + +// ToJSON serializes ThreadMarker to JSON string +func (obj *ThreadMarker) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ThreadMarker to YAML string +func (obj *ThreadMarker) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ThreadMarker from JSON string +func ThreadMarkerFromJSON(jsonStr string) (ThreadMarker, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ThreadMarker{}, err + } + ctx := NewLoadContext() + return LoadThreadMarker(data, ctx) +} + +// FromYAML creates ThreadMarker from YAML string +func ThreadMarkerFromYAML(yamlStr string) (ThreadMarker, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ThreadMarker{}, err + } + ctx := NewLoadContext() + return LoadThreadMarker(data, ctx) +} diff --git a/runtime/go/prompty/model/thread_marker_test.go b/runtime/go/prompty/model/thread_marker_test.go new file mode 100644 index 00000000..0848fa70 --- /dev/null +++ b/runtime/go/prompty/model/thread_marker_test.go @@ -0,0 +1,154 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestThreadMarkerLoadJSON tests loading ThreadMarker from JSON +func TestThreadMarkerLoadJSON(t *testing.T) { + jsonData := ` +{ + "name": "thread", + "kind": "thread" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThreadMarker(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThreadMarker: %v", err) + } + if instance.Name != "thread" { + t.Errorf(`Expected Name to be "thread", got %v`, instance.Name) + } + if instance.Kind != "thread" { + t.Errorf(`Expected Kind to be "thread", got %v`, instance.Kind) + } +} + +// TestThreadMarkerLoadYAML tests loading ThreadMarker from YAML +func TestThreadMarkerLoadYAML(t *testing.T) { + yamlData := ` +name: thread +kind: thread + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThreadMarker(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThreadMarker: %v", err) + } + if instance.Name != "thread" { + t.Errorf(`Expected Name to be "thread", got %v`, instance.Name) + } + if instance.Kind != "thread" { + t.Errorf(`Expected Kind to be "thread", got %v`, instance.Kind) + } +} + +// TestThreadMarkerRoundtrip tests load -> save -> load produces equivalent data +func TestThreadMarkerRoundtrip(t *testing.T) { + jsonData := ` +{ + "name": "thread", + "kind": "thread" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadThreadMarker(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ThreadMarker: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadThreadMarker(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ThreadMarker: %v", err) + } + if reloaded.Name != "thread" { + t.Errorf(`Expected Name to be "thread", got %v`, reloaded.Name) + } + if reloaded.Kind != "thread" { + t.Errorf(`Expected Kind to be "thread", got %v`, reloaded.Kind) + } +} + +// TestThreadMarkerToJSON tests that ToJSON produces valid JSON +func TestThreadMarkerToJSON(t *testing.T) { + jsonData := ` +{ + "name": "thread", + "kind": "thread" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThreadMarker(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThreadMarker: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestThreadMarkerToYAML tests that ToYAML produces valid YAML +func TestThreadMarkerToYAML(t *testing.T) { + jsonData := ` +{ + "name": "thread", + "kind": "thread" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadThreadMarker(data, ctx) + if err != nil { + t.Fatalf("Failed to load ThreadMarker: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/token_event_payload.go b/runtime/go/prompty/model/token_event_payload.go new file mode 100644 index 00000000..a569d5f2 --- /dev/null +++ b/runtime/go/prompty/model/token_event_payload.go @@ -0,0 +1,79 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// TokenEventPayload represents Payload for "token" events — a single text token streamed from the LLM. + +type TokenEventPayload struct { + Token string `json:"token" yaml:"token"` +} + +// LoadTokenEventPayload creates a TokenEventPayload from a map[string]interface{} +func LoadTokenEventPayload(data interface{}, ctx *LoadContext) (TokenEventPayload, error) { + result := TokenEventPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["token"]; ok && val != nil { + result.Token = val.(string) + } + } + + return result, nil +} + +// Save serializes TokenEventPayload to map[string]interface{} +func (obj *TokenEventPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["token"] = obj.Token + + return result +} + +// ToJSON serializes TokenEventPayload to JSON string +func (obj *TokenEventPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes TokenEventPayload to YAML string +func (obj *TokenEventPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates TokenEventPayload from JSON string +func TokenEventPayloadFromJSON(jsonStr string) (TokenEventPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return TokenEventPayload{}, err + } + ctx := NewLoadContext() + return LoadTokenEventPayload(data, ctx) +} + +// FromYAML creates TokenEventPayload from YAML string +func TokenEventPayloadFromYAML(yamlStr string) (TokenEventPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return TokenEventPayload{}, err + } + ctx := NewLoadContext() + return LoadTokenEventPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/token_event_payload_test.go b/runtime/go/prompty/model/token_event_payload_test.go new file mode 100644 index 00000000..1978b503 --- /dev/null +++ b/runtime/go/prompty/model/token_event_payload_test.go @@ -0,0 +1,140 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestTokenEventPayloadLoadJSON tests loading TokenEventPayload from JSON +func TestTokenEventPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "token": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenEventPayload: %v", err) + } + if instance.Token != "Hello" { + t.Errorf(`Expected Token to be "Hello", got %v`, instance.Token) + } +} + +// TestTokenEventPayloadLoadYAML tests loading TokenEventPayload from YAML +func TestTokenEventPayloadLoadYAML(t *testing.T) { + yamlData := ` +token: Hello + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenEventPayload: %v", err) + } + if instance.Token != "Hello" { + t.Errorf(`Expected Token to be "Hello", got %v`, instance.Token) + } +} + +// TestTokenEventPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestTokenEventPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "token": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenEventPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load TokenEventPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadTokenEventPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload TokenEventPayload: %v", err) + } + if reloaded.Token != "Hello" { + t.Errorf(`Expected Token to be "Hello", got %v`, reloaded.Token) + } +} + +// TestTokenEventPayloadToJSON tests that ToJSON produces valid JSON +func TestTokenEventPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "token": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenEventPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestTokenEventPayloadToYAML tests that ToYAML produces valid YAML +func TestTokenEventPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "token": "Hello" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenEventPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenEventPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/token_usage.go b/runtime/go/prompty/model/token_usage.go new file mode 100644 index 00000000..b6d1277e --- /dev/null +++ b/runtime/go/prompty/model/token_usage.go @@ -0,0 +1,149 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// TokenUsage represents Tracks token consumption for a single LLM call. Provider-specific field +// names (e.g., OpenAI's `prompt_tokens` vs Anthropic's `input_tokens`) +// are mapped via `knownAs` augments in the wire directory. + +type TokenUsage struct { + PromptTokens *int32 `json:"promptTokens,omitempty" yaml:"promptTokens,omitempty"` + CompletionTokens *int32 `json:"completionTokens,omitempty" yaml:"completionTokens,omitempty"` + TotalTokens *int32 `json:"totalTokens,omitempty" yaml:"totalTokens,omitempty"` +} + +// LoadTokenUsage creates a TokenUsage from a map[string]interface{} +func LoadTokenUsage(data interface{}, ctx *LoadContext) (TokenUsage, error) { + result := TokenUsage{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["promptTokens"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.PromptTokens = &v + } + if val, ok := m["completionTokens"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.CompletionTokens = &v + } + if val, ok := m["totalTokens"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.TotalTokens = &v + } + } + + return result, nil +} + +// Save serializes TokenUsage to map[string]interface{} +func (obj *TokenUsage) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + if obj.PromptTokens != nil { + result["promptTokens"] = *obj.PromptTokens + } + if obj.CompletionTokens != nil { + result["completionTokens"] = *obj.CompletionTokens + } + if obj.TotalTokens != nil { + result["totalTokens"] = *obj.TotalTokens + } + + return result +} + +// ToWire converts to provider-specific wire format. +func (obj *TokenUsage) ToWire(provider string) map[string]interface{} { + data := obj.Save(nil) + result := make(map[string]interface{}) + wireMap := map[string]map[string]string{ + "promptTokens": {"openai": "prompt_tokens", "anthropic": "input_tokens"}, + "completionTokens": {"openai": "completion_tokens", "anthropic": "output_tokens"}, + "totalTokens": {"openai": "total_tokens"}, + } + for key, value := range data { + if mapping, ok := wireMap[key]; ok { + if wireName, ok := mapping[provider]; ok { + result[wireName] = value + } + } + } + return result +} + +// ToJSON serializes TokenUsage to JSON string +func (obj *TokenUsage) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes TokenUsage to YAML string +func (obj *TokenUsage) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates TokenUsage from JSON string +func TokenUsageFromJSON(jsonStr string) (TokenUsage, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return TokenUsage{}, err + } + ctx := NewLoadContext() + return LoadTokenUsage(data, ctx) +} + +// FromYAML creates TokenUsage from YAML string +func TokenUsageFromYAML(yamlStr string) (TokenUsage, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return TokenUsage{}, err + } + ctx := NewLoadContext() + return LoadTokenUsage(data, ctx) +} diff --git a/runtime/go/prompty/model/token_usage_test.go b/runtime/go/prompty/model/token_usage_test.go new file mode 100644 index 00000000..6a512629 --- /dev/null +++ b/runtime/go/prompty/model/token_usage_test.go @@ -0,0 +1,168 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestTokenUsageLoadJSON tests loading TokenUsage from JSON +func TestTokenUsageLoadJSON(t *testing.T) { + jsonData := ` +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenUsage(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenUsage: %v", err) + } + if instance.PromptTokens == nil || *instance.PromptTokens != 150 { + t.Errorf(`Expected PromptTokens to be 150, got %v`, instance.PromptTokens) + } + if instance.CompletionTokens == nil || *instance.CompletionTokens != 42 { + t.Errorf(`Expected CompletionTokens to be 42, got %v`, instance.CompletionTokens) + } + if instance.TotalTokens == nil || *instance.TotalTokens != 192 { + t.Errorf(`Expected TotalTokens to be 192, got %v`, instance.TotalTokens) + } +} + +// TestTokenUsageLoadYAML tests loading TokenUsage from YAML +func TestTokenUsageLoadYAML(t *testing.T) { + yamlData := ` +promptTokens: 150 +completionTokens: 42 +totalTokens: 192 + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenUsage(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenUsage: %v", err) + } + if instance.PromptTokens == nil || *instance.PromptTokens != 150 { + t.Errorf(`Expected PromptTokens to be 150, got %v`, instance.PromptTokens) + } + if instance.CompletionTokens == nil || *instance.CompletionTokens != 42 { + t.Errorf(`Expected CompletionTokens to be 42, got %v`, instance.CompletionTokens) + } + if instance.TotalTokens == nil || *instance.TotalTokens != 192 { + t.Errorf(`Expected TotalTokens to be 192, got %v`, instance.TotalTokens) + } +} + +// TestTokenUsageRoundtrip tests load -> save -> load produces equivalent data +func TestTokenUsageRoundtrip(t *testing.T) { + jsonData := ` +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenUsage(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load TokenUsage: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadTokenUsage(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload TokenUsage: %v", err) + } + if reloaded.PromptTokens == nil || *reloaded.PromptTokens != 150 { + t.Errorf(`Expected PromptTokens to be 150, got %v`, reloaded.PromptTokens) + } + if reloaded.CompletionTokens == nil || *reloaded.CompletionTokens != 42 { + t.Errorf(`Expected CompletionTokens to be 42, got %v`, reloaded.CompletionTokens) + } + if reloaded.TotalTokens == nil || *reloaded.TotalTokens != 192 { + t.Errorf(`Expected TotalTokens to be 192, got %v`, reloaded.TotalTokens) + } +} + +// TestTokenUsageToJSON tests that ToJSON produces valid JSON +func TestTokenUsageToJSON(t *testing.T) { + jsonData := ` +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenUsage(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenUsage: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestTokenUsageToYAML tests that ToYAML produces valid YAML +func TestTokenUsageToYAML(t *testing.T) { + jsonData := ` +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTokenUsage(data, ctx) + if err != nil { + t.Fatalf("Failed to load TokenUsage: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_call.go b/runtime/go/prompty/model/tool_call.go new file mode 100644 index 00000000..37325db5 --- /dev/null +++ b/runtime/go/prompty/model/tool_call.go @@ -0,0 +1,90 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolCall represents A tool call requested by the LLM. Contains the function name and serialized +// arguments that should be dispatched to the appropriate tool handler. + +type ToolCall struct { + Id string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Arguments string `json:"arguments" yaml:"arguments"` +} + +// LoadToolCall creates a ToolCall from a map[string]interface{} +func LoadToolCall(data interface{}, ctx *LoadContext) (ToolCall, error) { + result := ToolCall{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["id"]; ok && val != nil { + result.Id = val.(string) + } + if val, ok := m["name"]; ok && val != nil { + result.Name = val.(string) + } + if val, ok := m["arguments"]; ok && val != nil { + result.Arguments = val.(string) + } + } + + return result, nil +} + +// Save serializes ToolCall to map[string]interface{} +func (obj *ToolCall) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["id"] = obj.Id + result["name"] = obj.Name + result["arguments"] = obj.Arguments + + return result +} + +// ToJSON serializes ToolCall to JSON string +func (obj *ToolCall) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolCall to YAML string +func (obj *ToolCall) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolCall from JSON string +func ToolCallFromJSON(jsonStr string) (ToolCall, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolCall{}, err + } + ctx := NewLoadContext() + return LoadToolCall(data, ctx) +} + +// FromYAML creates ToolCall from YAML string +func ToolCallFromYAML(yamlStr string) (ToolCall, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolCall{}, err + } + ctx := NewLoadContext() + return LoadToolCall(data, ctx) +} diff --git a/runtime/go/prompty/model/tool_call_start_payload.go b/runtime/go/prompty/model/tool_call_start_payload.go new file mode 100644 index 00000000..516ac1e8 --- /dev/null +++ b/runtime/go/prompty/model/tool_call_start_payload.go @@ -0,0 +1,84 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolCallStartPayload represents Payload for "tool_call_start" events — the LLM has requested a tool call. + +type ToolCallStartPayload struct { + Name string `json:"name" yaml:"name"` + Arguments string `json:"arguments" yaml:"arguments"` +} + +// LoadToolCallStartPayload creates a ToolCallStartPayload from a map[string]interface{} +func LoadToolCallStartPayload(data interface{}, ctx *LoadContext) (ToolCallStartPayload, error) { + result := ToolCallStartPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["name"]; ok && val != nil { + result.Name = val.(string) + } + if val, ok := m["arguments"]; ok && val != nil { + result.Arguments = val.(string) + } + } + + return result, nil +} + +// Save serializes ToolCallStartPayload to map[string]interface{} +func (obj *ToolCallStartPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["name"] = obj.Name + result["arguments"] = obj.Arguments + + return result +} + +// ToJSON serializes ToolCallStartPayload to JSON string +func (obj *ToolCallStartPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolCallStartPayload to YAML string +func (obj *ToolCallStartPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolCallStartPayload from JSON string +func ToolCallStartPayloadFromJSON(jsonStr string) (ToolCallStartPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolCallStartPayload{}, err + } + ctx := NewLoadContext() + return LoadToolCallStartPayload(data, ctx) +} + +// FromYAML creates ToolCallStartPayload from YAML string +func ToolCallStartPayloadFromYAML(yamlStr string) (ToolCallStartPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolCallStartPayload{}, err + } + ctx := NewLoadContext() + return LoadToolCallStartPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/tool_call_start_payload_test.go b/runtime/go/prompty/model/tool_call_start_payload_test.go new file mode 100644 index 00000000..28115bc8 --- /dev/null +++ b/runtime/go/prompty/model/tool_call_start_payload_test.go @@ -0,0 +1,154 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolCallStartPayloadLoadJSON tests loading ToolCallStartPayload from JSON +func TestToolCallStartPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCallStartPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCallStartPayload: %v", err) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } + if instance.Arguments != "{\"city\": \"Paris\"}" { + t.Errorf(`Expected Arguments to be "{\"city\": \"Paris\"}", got %v`, instance.Arguments) + } +} + +// TestToolCallStartPayloadLoadYAML tests loading ToolCallStartPayload from YAML +func TestToolCallStartPayloadLoadYAML(t *testing.T) { + yamlData := ` +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCallStartPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCallStartPayload: %v", err) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } + if instance.Arguments != "{\"city\": \"Paris\"}" { + t.Errorf(`Expected Arguments to be "{\"city\": \"Paris\"}", got %v`, instance.Arguments) + } +} + +// TestToolCallStartPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestToolCallStartPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCallStartPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolCallStartPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolCallStartPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolCallStartPayload: %v", err) + } + if reloaded.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, reloaded.Name) + } + if reloaded.Arguments != "{\"city\": \"Paris\"}" { + t.Errorf(`Expected Arguments to be "{\"city\": \"Paris\"}", got %v`, reloaded.Arguments) + } +} + +// TestToolCallStartPayloadToJSON tests that ToJSON produces valid JSON +func TestToolCallStartPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCallStartPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCallStartPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolCallStartPayloadToYAML tests that ToYAML produces valid YAML +func TestToolCallStartPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCallStartPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCallStartPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_call_test.go b/runtime/go/prompty/model/tool_call_test.go new file mode 100644 index 00000000..7649eb2d --- /dev/null +++ b/runtime/go/prompty/model/tool_call_test.go @@ -0,0 +1,168 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolCallLoadJSON tests loading ToolCall from JSON +func TestToolCallLoadJSON(t *testing.T) { + jsonData := ` +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCall(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCall: %v", err) + } + if instance.Id != "call_abc123" { + t.Errorf(`Expected Id to be "call_abc123", got %v`, instance.Id) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } + if instance.Arguments != "{\"city\": \"Paris\"}" { + t.Errorf(`Expected Arguments to be "{\"city\": \"Paris\"}", got %v`, instance.Arguments) + } +} + +// TestToolCallLoadYAML tests loading ToolCall from YAML +func TestToolCallLoadYAML(t *testing.T) { + yamlData := ` +id: call_abc123 +name: get_weather +arguments: "{\"city\": \"Paris\"}" + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCall(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCall: %v", err) + } + if instance.Id != "call_abc123" { + t.Errorf(`Expected Id to be "call_abc123", got %v`, instance.Id) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } + if instance.Arguments != "{\"city\": \"Paris\"}" { + t.Errorf(`Expected Arguments to be "{\"city\": \"Paris\"}", got %v`, instance.Arguments) + } +} + +// TestToolCallRoundtrip tests load -> save -> load produces equivalent data +func TestToolCallRoundtrip(t *testing.T) { + jsonData := ` +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCall(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolCall: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolCall(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolCall: %v", err) + } + if reloaded.Id != "call_abc123" { + t.Errorf(`Expected Id to be "call_abc123", got %v`, reloaded.Id) + } + if reloaded.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, reloaded.Name) + } + if reloaded.Arguments != "{\"city\": \"Paris\"}" { + t.Errorf(`Expected Arguments to be "{\"city\": \"Paris\"}", got %v`, reloaded.Arguments) + } +} + +// TestToolCallToJSON tests that ToJSON produces valid JSON +func TestToolCallToJSON(t *testing.T) { + jsonData := ` +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCall(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCall: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolCallToYAML tests that ToYAML produces valid YAML +func TestToolCallToYAML(t *testing.T) { + jsonData := ` +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolCall(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolCall: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_chunk_test.go b/runtime/go/prompty/model/tool_chunk_test.go new file mode 100644 index 00000000..89bc0f4c --- /dev/null +++ b/runtime/go/prompty/model/tool_chunk_test.go @@ -0,0 +1,153 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolChunkLoadJSON tests loading ToolChunk from JSON +func TestToolChunkLoadJSON(t *testing.T) { + jsonData := ` +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolChunk: %v", err) + } + _ = instance // No scalar properties to validate +} + +// TestToolChunkLoadYAML tests loading ToolChunk from YAML +func TestToolChunkLoadYAML(t *testing.T) { + yamlData := ` +toolCall: + id: call_abc123 + name: get_weather + arguments: "{\"city\": \"Paris\"}" + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolChunk: %v", err) + } + _ = instance // No scalar properties to validate +} + +// TestToolChunkRoundtrip tests load -> save -> load produces equivalent data +func TestToolChunkRoundtrip(t *testing.T) { + jsonData := ` +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolChunk(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolChunk: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolChunk(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolChunk: %v", err) + } + _ = reloaded // No scalar properties to validate +} + +// TestToolChunkToJSON tests that ToJSON produces valid JSON +func TestToolChunkToJSON(t *testing.T) { + jsonData := ` +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolChunk: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolChunkToYAML tests that ToYAML produces valid YAML +func TestToolChunkToYAML(t *testing.T) { + jsonData := ` +{ + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolChunk(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolChunk: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_context.go b/runtime/go/prompty/model/tool_context.go new file mode 100644 index 00000000..fe155968 --- /dev/null +++ b/runtime/go/prompty/model/tool_context.go @@ -0,0 +1,104 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolContext represents Context passed to tool handlers during agent loop execution. Provides +// access to the agent configuration, current conversation state, and +// arbitrary metadata for tool implementations that need broader context. + +type ToolContext struct { + Messages []Message `json:"messages" yaml:"messages"` + Metadata map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// LoadToolContext creates a ToolContext from a map[string]interface{} +func LoadToolContext(data interface{}, ctx *LoadContext) (ToolContext, error) { + result := ToolContext{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["messages"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Messages = make([]Message, len(arr)) + for i, v := range arr { + if item, ok := v.(map[string]interface{}); ok { + loaded, _ := LoadMessage(item, ctx) + result.Messages[i] = loaded + } + } + } + } + if val, ok := m["metadata"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + result.Metadata = m + } + } + } + + return result, nil +} + +// Save serializes ToolContext to map[string]interface{} +func (obj *ToolContext) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + if obj.Messages != nil { + arr := make([]interface{}, len(obj.Messages)) + for i, item := range obj.Messages { + arr[i] = item.Save(ctx) + } + result["messages"] = arr + } + if obj.Metadata != nil { + result["metadata"] = obj.Metadata + } + + return result +} + +// ToJSON serializes ToolContext to JSON string +func (obj *ToolContext) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolContext to YAML string +func (obj *ToolContext) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolContext from JSON string +func ToolContextFromJSON(jsonStr string) (ToolContext, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolContext{}, err + } + ctx := NewLoadContext() + return LoadToolContext(data, ctx) +} + +// FromYAML creates ToolContext from YAML string +func ToolContextFromYAML(yamlStr string) (ToolContext, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolContext{}, err + } + ctx := NewLoadContext() + return LoadToolContext(data, ctx) +} diff --git a/runtime/go/prompty/model/tool_context_test.go b/runtime/go/prompty/model/tool_context_test.go new file mode 100644 index 00000000..cd05d2f0 --- /dev/null +++ b/runtime/go/prompty/model/tool_context_test.go @@ -0,0 +1,143 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolContextLoadJSON tests loading ToolContext from JSON +func TestToolContextLoadJSON(t *testing.T) { + jsonData := ` +{ + "metadata": { + "userId": "user-123" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolContext(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolContext: %v", err) + } + _ = instance // No scalar properties to validate +} + +// TestToolContextLoadYAML tests loading ToolContext from YAML +func TestToolContextLoadYAML(t *testing.T) { + yamlData := ` +metadata: + userId: user-123 + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolContext(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolContext: %v", err) + } + _ = instance // No scalar properties to validate +} + +// TestToolContextRoundtrip tests load -> save -> load produces equivalent data +func TestToolContextRoundtrip(t *testing.T) { + jsonData := ` +{ + "metadata": { + "userId": "user-123" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolContext(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolContext: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolContext(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolContext: %v", err) + } + _ = reloaded // No scalar properties to validate +} + +// TestToolContextToJSON tests that ToJSON produces valid JSON +func TestToolContextToJSON(t *testing.T) { + jsonData := ` +{ + "metadata": { + "userId": "user-123" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolContext(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolContext: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolContextToYAML tests that ToYAML produces valid YAML +func TestToolContextToYAML(t *testing.T) { + jsonData := ` +{ + "metadata": { + "userId": "user-123" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolContext(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolContext: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_dispatch_result.go b/runtime/go/prompty/model/tool_dispatch_result.go new file mode 100644 index 00000000..67b7b48f --- /dev/null +++ b/runtime/go/prompty/model/tool_dispatch_result.go @@ -0,0 +1,95 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolDispatchResult represents The result of dispatching a single tool call. Pairs the tool call +// identifier with the tool's name and result for correlation in the +// agent loop's message assembly. + +type ToolDispatchResult struct { + ToolCallId string `json:"toolCallId" yaml:"toolCallId"` + Name string `json:"name" yaml:"name"` + Result ToolResult `json:"result" yaml:"result"` +} + +// LoadToolDispatchResult creates a ToolDispatchResult from a map[string]interface{} +func LoadToolDispatchResult(data interface{}, ctx *LoadContext) (ToolDispatchResult, error) { + result := ToolDispatchResult{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["toolCallId"]; ok && val != nil { + result.ToolCallId = val.(string) + } + if val, ok := m["name"]; ok && val != nil { + result.Name = val.(string) + } + if val, ok := m["result"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + loaded, _ := LoadToolResult(m, ctx) + result.Result = loaded + } + } + } + + return result, nil +} + +// Save serializes ToolDispatchResult to map[string]interface{} +func (obj *ToolDispatchResult) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["toolCallId"] = obj.ToolCallId + result["name"] = obj.Name + + result["result"] = obj.Result.Save(ctx) + + return result +} + +// ToJSON serializes ToolDispatchResult to JSON string +func (obj *ToolDispatchResult) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolDispatchResult to YAML string +func (obj *ToolDispatchResult) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolDispatchResult from JSON string +func ToolDispatchResultFromJSON(jsonStr string) (ToolDispatchResult, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolDispatchResult{}, err + } + ctx := NewLoadContext() + return LoadToolDispatchResult(data, ctx) +} + +// FromYAML creates ToolDispatchResult from YAML string +func ToolDispatchResultFromYAML(yamlStr string) (ToolDispatchResult, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolDispatchResult{}, err + } + ctx := NewLoadContext() + return LoadToolDispatchResult(data, ctx) +} diff --git a/runtime/go/prompty/model/tool_dispatch_result_test.go b/runtime/go/prompty/model/tool_dispatch_result_test.go new file mode 100644 index 00000000..5cfda48c --- /dev/null +++ b/runtime/go/prompty/model/tool_dispatch_result_test.go @@ -0,0 +1,190 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolDispatchResultLoadJSON tests loading ToolDispatchResult from JSON +func TestToolDispatchResultLoadJSON(t *testing.T) { + jsonData := ` +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolDispatchResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolDispatchResult: %v", err) + } + if instance.ToolCallId != "call_abc123" { + t.Errorf(`Expected ToolCallId to be "call_abc123", got %v`, instance.ToolCallId) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } +} + +// TestToolDispatchResultLoadYAML tests loading ToolDispatchResult from YAML +func TestToolDispatchResultLoadYAML(t *testing.T) { + yamlData := ` +toolCallId: call_abc123 +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolDispatchResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolDispatchResult: %v", err) + } + if instance.ToolCallId != "call_abc123" { + t.Errorf(`Expected ToolCallId to be "call_abc123", got %v`, instance.ToolCallId) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } +} + +// TestToolDispatchResultRoundtrip tests load -> save -> load produces equivalent data +func TestToolDispatchResultRoundtrip(t *testing.T) { + jsonData := ` +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolDispatchResult(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolDispatchResult: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolDispatchResult(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolDispatchResult: %v", err) + } + if reloaded.ToolCallId != "call_abc123" { + t.Errorf(`Expected ToolCallId to be "call_abc123", got %v`, reloaded.ToolCallId) + } + if reloaded.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, reloaded.Name) + } +} + +// TestToolDispatchResultToJSON tests that ToJSON produces valid JSON +func TestToolDispatchResultToJSON(t *testing.T) { + jsonData := ` +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolDispatchResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolDispatchResult: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolDispatchResultToYAML tests that ToYAML produces valid YAML +func TestToolDispatchResultToYAML(t *testing.T) { + jsonData := ` +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolDispatchResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolDispatchResult: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_result.go b/runtime/go/prompty/model/tool_result.go new file mode 100644 index 00000000..07571994 --- /dev/null +++ b/runtime/go/prompty/model/tool_result.go @@ -0,0 +1,118 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolResult represents The result of a tool execution. Contains a list of content parts, enabling +// rich tool results (text, images, files, audio) rather than just strings. +// +// Implementations MUST support conversion from a plain string to a ToolResult +// containing a single TextPart for backward compatibility. + +type ToolResult struct { + Parts []interface{} `json:"parts" yaml:"parts"` +} + +// LoadToolResult creates a ToolResult from a map[string]interface{} +func LoadToolResult(data interface{}, ctx *LoadContext) (ToolResult, error) { + result := ToolResult{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["parts"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Parts = make([]interface{}, len(arr)) + for i, v := range arr { + if item, ok := v.(map[string]interface{}); ok { + loaded, _ := LoadContentPart(item, ctx) + // Polymorphic type - store as interface{} + result.Parts[i] = loaded + } + } + } + } + } + + return result, nil +} + +// Save serializes ToolResult to map[string]interface{} +func (obj *ToolResult) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + if obj.Parts != nil { + arr := make([]interface{}, len(obj.Parts)) + for i, item := range obj.Parts { + // Handle polymorphic type via type switch + switch v := item.(type) { + case interface { + Save(*SaveContext) map[string]interface{} + }: + arr[i] = v.Save(ctx) + default: + arr[i] = item + } + } + result["parts"] = arr + } + + return result +} + +// ToJSON serializes ToolResult to JSON string +func (obj *ToolResult) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolResult to YAML string +func (obj *ToolResult) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolResult from JSON string +func ToolResultFromJSON(jsonStr string) (ToolResult, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolResult{}, err + } + ctx := NewLoadContext() + return LoadToolResult(data, ctx) +} + +// FromYAML creates ToolResult from YAML string +func ToolResultFromYAML(yamlStr string) (ToolResult, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolResult{}, err + } + ctx := NewLoadContext() + return LoadToolResult(data, ctx) +} + +// NewTextToolResult creates a ToolResult with preset field values. +func NewTextToolResult(value string) ToolResult { + return ToolResult{Parts: []interface{}{TextPart{Kind: "text", Value: value}}} +} + +// ToolResultHelpers defines helper methods for ToolResult. +// Implement these in a separate file (e.g., toolresult_helpers.go). +type ToolResultHelpers interface { + // Text — Concatenate all TextPart values joined by newline + Text() string +} diff --git a/runtime/go/prompty/model/tool_result_payload.go b/runtime/go/prompty/model/tool_result_payload.go new file mode 100644 index 00000000..45b9e1e3 --- /dev/null +++ b/runtime/go/prompty/model/tool_result_payload.go @@ -0,0 +1,88 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolResultPayload represents Payload for "tool_result" events — a tool has returned its result. + +type ToolResultPayload struct { + Name string `json:"name" yaml:"name"` + Result ToolResult `json:"result" yaml:"result"` +} + +// LoadToolResultPayload creates a ToolResultPayload from a map[string]interface{} +func LoadToolResultPayload(data interface{}, ctx *LoadContext) (ToolResultPayload, error) { + result := ToolResultPayload{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["name"]; ok && val != nil { + result.Name = val.(string) + } + if val, ok := m["result"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + loaded, _ := LoadToolResult(m, ctx) + result.Result = loaded + } + } + } + + return result, nil +} + +// Save serializes ToolResultPayload to map[string]interface{} +func (obj *ToolResultPayload) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["name"] = obj.Name + + result["result"] = obj.Result.Save(ctx) + + return result +} + +// ToJSON serializes ToolResultPayload to JSON string +func (obj *ToolResultPayload) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolResultPayload to YAML string +func (obj *ToolResultPayload) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolResultPayload from JSON string +func ToolResultPayloadFromJSON(jsonStr string) (ToolResultPayload, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolResultPayload{}, err + } + ctx := NewLoadContext() + return LoadToolResultPayload(data, ctx) +} + +// FromYAML creates ToolResultPayload from YAML string +func ToolResultPayloadFromYAML(yamlStr string) (ToolResultPayload, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolResultPayload{}, err + } + ctx := NewLoadContext() + return LoadToolResultPayload(data, ctx) +} diff --git a/runtime/go/prompty/model/tool_result_payload_test.go b/runtime/go/prompty/model/tool_result_payload_test.go new file mode 100644 index 00000000..86ab959d --- /dev/null +++ b/runtime/go/prompty/model/tool_result_payload_test.go @@ -0,0 +1,176 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolResultPayloadLoadJSON tests loading ToolResultPayload from JSON +func TestToolResultPayloadLoadJSON(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResultPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResultPayload: %v", err) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } +} + +// TestToolResultPayloadLoadYAML tests loading ToolResultPayload from YAML +func TestToolResultPayloadLoadYAML(t *testing.T) { + yamlData := ` +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResultPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResultPayload: %v", err) + } + if instance.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, instance.Name) + } +} + +// TestToolResultPayloadRoundtrip tests load -> save -> load produces equivalent data +func TestToolResultPayloadRoundtrip(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResultPayload(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolResultPayload: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolResultPayload(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolResultPayload: %v", err) + } + if reloaded.Name != "get_weather" { + t.Errorf(`Expected Name to be "get_weather", got %v`, reloaded.Name) + } +} + +// TestToolResultPayloadToJSON tests that ToJSON produces valid JSON +func TestToolResultPayloadToJSON(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResultPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResultPayload: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolResultPayloadToYAML tests that ToYAML produces valid YAML +func TestToolResultPayloadToYAML(t *testing.T) { + jsonData := ` +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResultPayload(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResultPayload: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_result_test.go b/runtime/go/prompty/model/tool_result_test.go new file mode 100644 index 00000000..cd2c39af --- /dev/null +++ b/runtime/go/prompty/model/tool_result_test.go @@ -0,0 +1,156 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestToolResultLoadJSON tests loading ToolResult from JSON +func TestToolResultLoadJSON(t *testing.T) { + jsonData := ` +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResult: %v", err) + } + _ = instance // No scalar properties to validate +} + +// TestToolResultLoadYAML tests loading ToolResult from YAML +func TestToolResultLoadYAML(t *testing.T) { + yamlData := ` +parts: + - kind: text + value: 72°F and sunny + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResult: %v", err) + } + _ = instance // No scalar properties to validate +} + +// TestToolResultRoundtrip tests load -> save -> load produces equivalent data +func TestToolResultRoundtrip(t *testing.T) { + jsonData := ` +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResult(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolResult: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadToolResult(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolResult: %v", err) + } + _ = reloaded // No scalar properties to validate +} + +// TestToolResultToJSON tests that ToJSON produces valid JSON +func TestToolResultToJSON(t *testing.T) { + jsonData := ` +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResult: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolResultToYAML tests that ToYAML produces valid YAML +func TestToolResultToYAML(t *testing.T) { + jsonData := ` +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadToolResult(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolResult: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/prompty/model/tool_test.go b/runtime/go/prompty/model/tool_test.go index cfe11413..e3e3fba4 100644 --- a/runtime/go/prompty/model/tool_test.go +++ b/runtime/go/prompty/model/tool_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/microsoft/agentschema-go/prompty" + "prompty/model" ) // TestToolLoadJSON tests loading Tool from JSON diff --git a/runtime/go/prompty/model/turn_options.go b/runtime/go/prompty/model/turn_options.go new file mode 100644 index 00000000..7497a74e --- /dev/null +++ b/runtime/go/prompty/model/turn_options.go @@ -0,0 +1,177 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// TurnOptions represents Configuration for the agent loop's turn() function. Controls iteration +// limits, retry policy, context management, and execution behavior. +// +// Runtimes accept these as either a TurnOptions object or individual +// keyword/named parameters — the TypeSpec model defines the canonical +// field set. + +type TurnOptions struct { + MaxIterations *int32 `json:"maxIterations,omitempty" yaml:"maxIterations,omitempty"` + MaxLlmRetries *int32 `json:"maxLlmRetries,omitempty" yaml:"maxLlmRetries,omitempty"` + ContextBudget *int32 `json:"contextBudget,omitempty" yaml:"contextBudget,omitempty"` + ParallelToolCalls *bool `json:"parallelToolCalls,omitempty" yaml:"parallelToolCalls,omitempty"` + Raw *bool `json:"raw,omitempty" yaml:"raw,omitempty"` + Turn *int32 `json:"turn,omitempty" yaml:"turn,omitempty"` + Compaction *CompactionConfig `json:"compaction,omitempty" yaml:"compaction,omitempty"` +} + +// LoadTurnOptions creates a TurnOptions from a map[string]interface{} +func LoadTurnOptions(data interface{}, ctx *LoadContext) (TurnOptions, error) { + result := TurnOptions{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["maxIterations"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.MaxIterations = &v + } + if val, ok := m["maxLlmRetries"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.MaxLlmRetries = &v + } + if val, ok := m["contextBudget"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.ContextBudget = &v + } + if val, ok := m["parallelToolCalls"]; ok && val != nil { + v := val.(bool) + result.ParallelToolCalls = &v + } + if val, ok := m["raw"]; ok && val != nil { + v := val.(bool) + result.Raw = &v + } + if val, ok := m["turn"]; ok && val != nil { // Handle various numeric types from JSON/YAML/roundtrip + var v int32 + switch n := val.(type) { + case int: + v = int32(n) + case int32: + v = int32(n) + case int64: + v = int32(n) + case float64: + v = int32(n) + } + result.Turn = &v + } + if val, ok := m["compaction"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + loaded, _ := LoadCompactionConfig(m, ctx) + result.Compaction = &loaded + } + } + } + + return result, nil +} + +// Save serializes TurnOptions to map[string]interface{} +func (obj *TurnOptions) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + if obj.MaxIterations != nil { + result["maxIterations"] = *obj.MaxIterations + } + if obj.MaxLlmRetries != nil { + result["maxLlmRetries"] = *obj.MaxLlmRetries + } + if obj.ContextBudget != nil { + result["contextBudget"] = *obj.ContextBudget + } + if obj.ParallelToolCalls != nil { + result["parallelToolCalls"] = *obj.ParallelToolCalls + } + if obj.Raw != nil { + result["raw"] = *obj.Raw + } + if obj.Turn != nil { + result["turn"] = *obj.Turn + } + if obj.Compaction != nil { + result["compaction"] = obj.Compaction.Save(ctx) + } + + return result +} + +// ToJSON serializes TurnOptions to JSON string +func (obj *TurnOptions) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes TurnOptions to YAML string +func (obj *TurnOptions) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates TurnOptions from JSON string +func TurnOptionsFromJSON(jsonStr string) (TurnOptions, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return TurnOptions{}, err + } + ctx := NewLoadContext() + return LoadTurnOptions(data, ctx) +} + +// FromYAML creates TurnOptions from YAML string +func TurnOptionsFromYAML(yamlStr string) (TurnOptions, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return TurnOptions{}, err + } + ctx := NewLoadContext() + return LoadTurnOptions(data, ctx) +} diff --git a/runtime/go/prompty/model/turn_options_test.go b/runtime/go/prompty/model/turn_options_test.go new file mode 100644 index 00000000..2bae1e0d --- /dev/null +++ b/runtime/go/prompty/model/turn_options_test.go @@ -0,0 +1,224 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package prompty_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "prompty/model" +) + +// TestTurnOptionsLoadJSON tests loading TurnOptions from JSON +func TestTurnOptionsLoadJSON(t *testing.T) { + jsonData := ` +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTurnOptions(data, ctx) + if err != nil { + t.Fatalf("Failed to load TurnOptions: %v", err) + } + if instance.MaxIterations == nil || *instance.MaxIterations != 10 { + t.Errorf(`Expected MaxIterations to be 10, got %v`, instance.MaxIterations) + } + if instance.MaxLlmRetries == nil || *instance.MaxLlmRetries != 3 { + t.Errorf(`Expected MaxLlmRetries to be 3, got %v`, instance.MaxLlmRetries) + } + if instance.ContextBudget == nil || *instance.ContextBudget != 100000 { + t.Errorf(`Expected ContextBudget to be 100000, got %v`, instance.ContextBudget) + } + if instance.ParallelToolCalls == nil || *instance.ParallelToolCalls != true { + t.Errorf(`Expected ParallelToolCalls to be true, got %v`, instance.ParallelToolCalls) + } + if instance.Raw == nil || *instance.Raw != false { + t.Errorf(`Expected Raw to be false, got %v`, instance.Raw) + } + if instance.Turn == nil || *instance.Turn != 1 { + t.Errorf(`Expected Turn to be 1, got %v`, instance.Turn) + } +} + +// TestTurnOptionsLoadYAML tests loading TurnOptions from YAML +func TestTurnOptionsLoadYAML(t *testing.T) { + yamlData := ` +maxIterations: 10 +maxLlmRetries: 3 +contextBudget: 100000 +parallelToolCalls: true +raw: false +turn: 1 +compaction: + strategy: summarize + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTurnOptions(data, ctx) + if err != nil { + t.Fatalf("Failed to load TurnOptions: %v", err) + } + if instance.MaxIterations == nil || *instance.MaxIterations != 10 { + t.Errorf(`Expected MaxIterations to be 10, got %v`, instance.MaxIterations) + } + if instance.MaxLlmRetries == nil || *instance.MaxLlmRetries != 3 { + t.Errorf(`Expected MaxLlmRetries to be 3, got %v`, instance.MaxLlmRetries) + } + if instance.ContextBudget == nil || *instance.ContextBudget != 100000 { + t.Errorf(`Expected ContextBudget to be 100000, got %v`, instance.ContextBudget) + } + if instance.ParallelToolCalls == nil || *instance.ParallelToolCalls != true { + t.Errorf(`Expected ParallelToolCalls to be true, got %v`, instance.ParallelToolCalls) + } + if instance.Raw == nil || *instance.Raw != false { + t.Errorf(`Expected Raw to be false, got %v`, instance.Raw) + } + if instance.Turn == nil || *instance.Turn != 1 { + t.Errorf(`Expected Turn to be 1, got %v`, instance.Turn) + } +} + +// TestTurnOptionsRoundtrip tests load -> save -> load produces equivalent data +func TestTurnOptionsRoundtrip(t *testing.T) { + jsonData := ` +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := prompty.NewLoadContext() + instance, err := prompty.LoadTurnOptions(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load TurnOptions: %v", err) + } + saveCtx := prompty.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := prompty.LoadTurnOptions(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload TurnOptions: %v", err) + } + if reloaded.MaxIterations == nil || *reloaded.MaxIterations != 10 { + t.Errorf(`Expected MaxIterations to be 10, got %v`, reloaded.MaxIterations) + } + if reloaded.MaxLlmRetries == nil || *reloaded.MaxLlmRetries != 3 { + t.Errorf(`Expected MaxLlmRetries to be 3, got %v`, reloaded.MaxLlmRetries) + } + if reloaded.ContextBudget == nil || *reloaded.ContextBudget != 100000 { + t.Errorf(`Expected ContextBudget to be 100000, got %v`, reloaded.ContextBudget) + } + if reloaded.ParallelToolCalls == nil || *reloaded.ParallelToolCalls != true { + t.Errorf(`Expected ParallelToolCalls to be true, got %v`, reloaded.ParallelToolCalls) + } + if reloaded.Raw == nil || *reloaded.Raw != false { + t.Errorf(`Expected Raw to be false, got %v`, reloaded.Raw) + } + if reloaded.Turn == nil || *reloaded.Turn != 1 { + t.Errorf(`Expected Turn to be 1, got %v`, reloaded.Turn) + } +} + +// TestTurnOptionsToJSON tests that ToJSON produces valid JSON +func TestTurnOptionsToJSON(t *testing.T) { + jsonData := ` +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTurnOptions(data, ctx) + if err != nil { + t.Fatalf("Failed to load TurnOptions: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestTurnOptionsToYAML tests that ToYAML produces valid YAML +func TestTurnOptionsToYAML(t *testing.T) { + jsonData := ` +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := prompty.NewLoadContext() + instance, err := prompty.LoadTurnOptions(data, ctx) + if err != nil { + t.Fatalf("Failed to load TurnOptions: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/python/prompty/.venv_new/Scripts/Activate.ps1 b/runtime/python/prompty/.venv_new/Scripts/Activate.ps1 new file mode 100644 index 00000000..dbbf985c --- /dev/null +++ b/runtime/python/prompty/.venv_new/Scripts/Activate.ps1 @@ -0,0 +1,502 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" + +# SIG # Begin signature block +# MIIvIwYJKoZIhvcNAQcCoIIvFDCCLxACAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBnL745ElCYk8vk +# dBtMuQhLeWJ3ZGfzKW4DHCYzAn+QB6CCE8MwggWQMIIDeKADAgECAhAFmxtXno4h +# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK +# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV +# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z +# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ +# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 +# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z +# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ +# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s +# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL +# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb +# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 +# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c +# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx +# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 +# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL +# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud +# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf +# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk +# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS +# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK +# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB +# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp +# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg +# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri +# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 +# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 +# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 +# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H +# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G +# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ +# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 +# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla +# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE +# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz +# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C +# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce +# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da +# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T +# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA +# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh +# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM +# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z +# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 +# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY +# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP +# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T +# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD +# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG +# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY +# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj +# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV +# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU +# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN +# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry +# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL +# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf +# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh +# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh +# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV +# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j +# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH +# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC +# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l +# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW +# eE4wggd3MIIFX6ADAgECAhAHHxQbizANJfMU6yMM0NHdMA0GCSqGSIb3DQEBCwUA +# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE +# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz +# ODQgMjAyMSBDQTEwHhcNMjIwMTE3MDAwMDAwWhcNMjUwMTE1MjM1OTU5WjB8MQsw +# CQYDVQQGEwJVUzEPMA0GA1UECBMGT3JlZ29uMRIwEAYDVQQHEwlCZWF2ZXJ0b24x +# IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMSMwIQYDVQQDExpQ +# eXRob24gU29mdHdhcmUgRm91bmRhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +# ADCCAgoCggIBAKgc0BTT+iKbtK6f2mr9pNMUTcAJxKdsuOiSYgDFfwhjQy89koM7 +# uP+QV/gwx8MzEt3c9tLJvDccVWQ8H7mVsk/K+X+IufBLCgUi0GGAZUegEAeRlSXx +# xhYScr818ma8EvGIZdiSOhqjYc4KnfgfIS4RLtZSrDFG2tN16yS8skFa3IHyvWdb +# D9PvZ4iYNAS4pjYDRjT/9uzPZ4Pan+53xZIcDgjiTwOh8VGuppxcia6a7xCyKoOA +# GjvCyQsj5223v1/Ig7Dp9mGI+nh1E3IwmyTIIuVHyK6Lqu352diDY+iCMpk9Zanm +# SjmB+GMVs+H/gOiofjjtf6oz0ki3rb7sQ8fTnonIL9dyGTJ0ZFYKeb6BLA66d2GA +# LwxZhLe5WH4Np9HcyXHACkppsE6ynYjTOd7+jN1PRJahN1oERzTzEiV6nCO1M3U1 +# HbPTGyq52IMFSBM2/07WTJSbOeXjvYR7aUxK9/ZkJiacl2iZI7IWe7JKhHohqKuc +# eQNyOzxTakLcRkzynvIrk33R9YVqtB4L6wtFxhUjvDnQg16xot2KVPdfyPAWd81w +# tZADmrUtsZ9qG79x1hBdyOl4vUtVPECuyhCxaw+faVjumapPUnwo8ygflJJ74J+B +# Yxf6UuD7m8yzsfXWkdv52DjL74TxzuFTLHPyARWCSCAbzn3ZIly+qIqDAgMBAAGj +# ggIGMIICAjAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5hewiIZfROQjAdBgNVHQ4E +# FgQUt/1Teh2XDuUj2WW3siYWJgkZHA8wDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM +# MAoGCCsGAQUFBwMDMIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRp +# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI +# QTM4NDIwMjFDQTEuY3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20v +# RGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0Ex +# LmNybDA+BgNVHSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8v +# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUF +# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6 +# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu +# aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZI +# hvcNAQELBQADggIBABxv4AeV/5ltkELHSC63fXAFYS5tadcWTiNc2rskrNLrfH1N +# s0vgSZFoQxYBFKI159E8oQQ1SKbTEubZ/B9kmHPhprHya08+VVzxC88pOEvz68nA +# 82oEM09584aILqYmj8Pj7h/kmZNzuEL7WiwFa/U1hX+XiWfLIJQsAHBla0i7QRF2 +# de8/VSF0XXFa2kBQ6aiTsiLyKPNbaNtbcucaUdn6vVUS5izWOXM95BSkFSKdE45O +# q3FForNJXjBvSCpwcP36WklaHL+aHu1upIhCTUkzTHMh8b86WmjRUqbrnvdyR2yd +# I5l1OqcMBjkpPpIV6wcc+KY/RH2xvVuuoHjlUjwq2bHiNoX+W1scCpnA8YTs2d50 +# jDHUgwUo+ciwpffH0Riq132NFmrH3r67VaN3TuBxjI8SIZM58WEDkbeoriDk3hxU +# 8ZWV7b8AW6oyVBGfM06UgkfMb58h+tJPrFx8VI/WLq1dTqMfZOm5cuclMnUHs2uq +# rRNtnV8UfidPBL4ZHkTcClQbCoz0UbLhkiDvIS00Dn+BBcxw/TKqVL4Oaz3bkMSs +# M46LciTeucHY9ExRVt3zy7i149sd+F4QozPqn7FrSVHXmem3r7bjyHTxOgqxRCVa +# 18Vtx7P/8bYSBeS+WHCKcliFCecspusCDSlnRUjZwyPdP0VHxaZg2unjHY3rMYIa +# tjCCGrICAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu +# Yy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJT +# QTQwOTYgU0hBMzg0IDIwMjEgQ0ExAhAHHxQbizANJfMU6yMM0NHdMA0GCWCGSAFl +# AwQCAQUAoIHIMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC +# AQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBnAZ6P7YvTwq0fbF62 +# o7E75R0LxsW5OtyYiFESQckLhjBcBgorBgEEAYI3AgEMMU4wTKBGgEQAQgB1AGkA +# bAB0ADoAIABSAGUAbABlAGEAcwBlAF8AdgAzAC4AMQAxAC4AOQBfADIAMAAyADQA +# MAA0ADAAMgAuADAAMqECgAAwDQYJKoZIhvcNAQEBBQAEggIAg5wQO5NiKFirm9vr +# 1//X5G4t+z318Uagu8vJT/vTjkMTau86CF+SwP3pqC1H2ZMUyVYmVHae5dswKAMR +# hHY1VJV/0lJI+LdYcaxHI/WYzaFLbDrQI/Mty5cabjveG6geMlcJG4nYZlyQX+fJ +# 1k0ogeIF1owldecXP8t5e10WlHBlWb8IBnIPwMtJVZ2/y8NASxsnSJE7pEe7ijGe +# 5Bv9DXvoltKnMSVYv9u2vn7PeIq+Jm3n3kOGSIYtfdytEd1Fd6spfdcmIhqyzVk0 +# Hslq7Aqd7soT0xdmNa/amzEA4HRHpWGUhzOtcC+EqEIIJk9kTjyVgCiyWaB5gGko +# OAZfsxQn+a916iWwA7RrQ+TzBZq/pleUTLZzJmI3DXFjuJ1NDP6Sdw6KREgx6Yw4 +# q2NnnodKlGZkMDcGYPTM2sA4i6i6FsznWY4d8wE4J261YeUrVfIyTx+Q81W4KXoi +# C0x7Pe9Bjh4oJGM3YiLyhVL56sXZWxAC2C/vD3nvIvra9EpvlMvQh6b0xl0V4TSN +# dJ7T7VttR/WNjau46JIgbGZWCDBTTUAydQNoAZ4KnCrcIZCN6Y0qVokXsYHsVIto +# TsnM2+Ca09wxuOIfCOSKpAmqdJ/w2NwLwp+0gwrO2uzpCfbSbkAd+UQNv0joPyUp +# ywmsQndxqA8TaADp8TfkkpJywJGhghc/MIIXOwYKKwYBBAGCNwMDATGCFyswghcn +# BgkqhkiG9w0BBwKgghcYMIIXFAIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3 +# DQEJEAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQg+Jhe +# IOzOttA8vliuL+r3CiY4EJTzfvPasXkI/vwkoI8CEHZ95Ht1TmSmU8+fM0kIG00Y +# DzIwMjQwNDAyMTIzMjEwWqCCEwkwggbCMIIEqqADAgECAhAFRK/zlJ0IOaa/2z9f +# 5WEWMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp +# Q2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2 +# IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjMwNzE0MDAwMDAwWhcNMzQxMDEz +# MjM1OTU5WjBIMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x +# IDAeBgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIzMIICIjANBgkqhkiG9w0B +# AQEFAAOCAg8AMIICCgKCAgEAo1NFhx2DjlusPlSzI+DPn9fl0uddoQ4J3C9Io5d6 +# OyqcZ9xiFVjBqZMRp82qsmrdECmKHmJjadNYnDVxvzqX65RQjxwg6seaOy+WZuNp +# 52n+W8PWKyAcwZeUtKVQgfLPywemMGjKg0La/H8JJJSkghraarrYO8pd3hkYhftF +# 6g1hbJ3+cV7EBpo88MUueQ8bZlLjyNY+X9pD04T10Mf2SC1eRXWWdf7dEKEbg8G4 +# 5lKVtUfXeCk5a+B4WZfjRCtK1ZXO7wgX6oJkTf8j48qG7rSkIWRw69XloNpjsy7p +# Be6q9iT1HbybHLK3X9/w7nZ9MZllR1WdSiQvrCuXvp/k/XtzPjLuUjT71Lvr1KAs +# NJvj3m5kGQc3AZEPHLVRzapMZoOIaGK7vEEbeBlt5NkP4FhB+9ixLOFRr7StFQYU +# 6mIIE9NpHnxkTZ0P387RXoyqq1AVybPKvNfEO2hEo6U7Qv1zfe7dCv95NBB+plwK +# WEwAPoVpdceDZNZ1zY8SdlalJPrXxGshuugfNJgvOuprAbD3+yqG7HtSOKmYCaFx +# smxxrz64b5bV4RAT/mFHCoz+8LbH1cfebCTwv0KCyqBxPZySkwS0aXAnDU+3tTbR +# yV8IpHCj7ArxES5k4MsiK8rxKBMhSVF+BmbTO77665E42FEHypS34lCh8zrTioPL +# QHsCAwEAAaOCAYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYG +# A1UdJQEB/wQMMAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCG +# SAGG/WwHATAfBgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4E +# FgQUpbbvE+fvzdBkodVWqWUxo97V40kwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDov +# L2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1 +# NlRpbWVTdGFtcGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUH +# MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDov +# L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNI +# QTI1NlRpbWVTdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAgRrW3qCp +# tZgXvHCNT4o8aJzYJf/LLOTN6l0ikuyMIgKpuM+AqNnn48XtJoKKcS8Y3U623mzX +# 4WCcK+3tPUiOuGu6fF29wmE3aEl3o+uQqhLXJ4Xzjh6S2sJAOJ9dyKAuJXglnSoF +# eoQpmLZXeY/bJlYrsPOnvTcM2Jh2T1a5UsK2nTipgedtQVyMadG5K8TGe8+c+nji +# kxp2oml101DkRBK+IA2eqUTQ+OVJdwhaIcW0z5iVGlS6ubzBaRm6zxbygzc0brBB +# Jt3eWpdPM43UjXd9dUWhpVgmagNF3tlQtVCMr1a9TMXhRsUo063nQwBw3syYnhmJ +# A+rUkTfvTVLzyWAhxFZH7doRS4wyw4jmWOK22z75X7BC1o/jF5HRqsBV44a/rCcs +# QdCaM0qoNtS5cpZ+l3k4SF/Kwtw9Mt911jZnWon49qfH5U81PAC9vpwqbHkB3NpE +# 5jreODsHXjlY9HxzMVWggBHLFAx+rrz+pOt5Zapo1iLKO+uagjVXKBbLafIymrLS +# 2Dq4sUaGa7oX/cR3bBVsrquvczroSUa31X/MtjjA2Owc9bahuEMs305MfR5ocMB3 +# CtQC4Fxguyj/OOVSWtasFyIjTvTs0xf7UGv/B3cfcZdEQcm4RtNsMnxYL2dHZeUb +# c7aZ+WssBkbvQR7w8F/g29mtkIBEr4AQQYowggauMIIElqADAgECAhAHNje3JFR8 +# 2Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK +# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV +# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0z +# NzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg +# SW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1 +# NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +# AQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI +# 82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9 +# xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ +# 3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5Emfv +# DqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDET +# qVcplicu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHe +# IhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jo +# n7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ +# 9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/T +# Xkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJg +# o1gJASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkw +# EgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+e +# yG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQD +# AgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEF +# BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRw +# Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy +# dDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln +# aUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg +# hkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGw +# GC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0 +# MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1D +# X+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw +# 1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY +# +/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0I +# SQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr +# 5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7y +# Rp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDop +# hrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/ +# AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMO +# Hds3OBqhK/bt1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkq +# hkiG9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j +# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBB +# c3N1cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5 +# WjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +# ExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJv +# b3QgRzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1K +# PDAiMGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2r +# snnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C +# 8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBf +# sXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +# QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8 +# rhsDdV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaY +# dj1ZXUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+ +# wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw +# ++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+N +# P8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7F +# wI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUw +# AwEB/zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAU +# Reuir/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEB +# BG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsG +# AQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1 +# cmVkSURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRp +# Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAow +# CDAGBgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/ +# Vwe9mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLe +# JLxSA8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE +# 1Od/6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9Hda +# XFSMb++hUD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbO +# byMt9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYID +# djCCA3ICAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu +# Yy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYg +# VGltZVN0YW1waW5nIENBAhAFRK/zlJ0IOaa/2z9f5WEWMA0GCWCGSAFlAwQCAQUA +# oIHRMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcN +# MjQwNDAyMTIzMjEwWjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBRm8CsywsLJD4Jd +# zqqKycZPGZzPQDAvBgkqhkiG9w0BCQQxIgQg58bvIvjFkyBb2O0xwLgtU8RJLcMV +# kvIdXiq3TCLuhq4wNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQg0vbkbe10IszR1EBX +# aEE2b4KK2lWarjMWr00amtQMeCgwDQYJKoZIhvcNAQEBBQAEggIAhU3imuIvql4+ +# IqPz0Anf0ZIB5hbafNTx1WEVhPEG9iJr24gpjbWvepQrbWJf0FBj8wY9GeRab6iv +# 79MMxZkPpR/DMK1qFr1vIlw67JhpqqNkaNIa5G3pAHDYdHYcB+Utw1p5XPOBRu0A +# f4wQ5fwWugys4CGGAboq4prLNRKeUGVexMDK7Eorsv9xmzK0tE9QSMA3SxLCcSIX +# mrMkKzTR3vn0dqaDG4Ge7U2w7dVnQYGBX+s6C9CCjvCtenCAQLbF+OyYhkMNDVtJ +# lTmzxxwyyA5fFZJpG/Wfo/84/P8lQXUTuwOBpFoLE65OqNEG03SoqKsW4aTqkVM7 +# b6fKLsygm1w23+UlHGF/fbExeqxgOZiuJWWt/OFy9T3HIcAF1SMh7mot5ciu7btS +# xjkr/fhsi1M3M1g/giyn0I8N24mgaICPtXAzAbZW7GSC0R5T2qnW6gYoAcY62Qdz +# jl/Ey1rnOQ26TuQODyPVHhfhoIBbdIDpDJ2Vu2mxyxUnjATbizphcBgsU1fBYvZR +# v+SuK1MYZOGqgzugfiufdeFAlBDA/e64yRkJvDBEkcyGvj6FS6nVm7ekJpJhLU3z +# sSSmcYwdx1YQCr48HEjcmGrj5sAzzg4U4WU/GrLWz2sSRmh5rKcDAa0ewfYi13Z2 +# a/cdr8Or2RQ5ZSQ8OHgr3GBw7koDWR8= +# SIG # End signature block diff --git a/runtime/python/prompty/.venv_new/Scripts/activate b/runtime/python/prompty/.venv_new/Scripts/activate new file mode 100644 index 00000000..c8b55d6e --- /dev/null +++ b/runtime/python/prompty/.venv_new/Scripts/activate @@ -0,0 +1,63 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="C:\Users\sejuare\.copilot\copilot-worktrees\prompty\sethjuarez-unsalivated-sharan\runtime\python\prompty\.venv_new" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/Scripts:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(.venv_new) ${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT="(.venv_new) " + export VIRTUAL_ENV_PROMPT +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/runtime/python/prompty/.venv_new/Scripts/activate.bat b/runtime/python/prompty/.venv_new/Scripts/activate.bat new file mode 100644 index 00000000..c0a74052 --- /dev/null +++ b/runtime/python/prompty/.venv_new/Scripts/activate.bat @@ -0,0 +1,34 @@ +@echo off + +rem This file is UTF-8 encoded, so we need to update the current code page while executing it +for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do ( + set _OLD_CODEPAGE=%%a +) +if defined _OLD_CODEPAGE ( + "%SystemRoot%\System32\chcp.com" 65001 > nul +) + +set VIRTUAL_ENV=C:\Users\sejuare\.copilot\copilot-worktrees\prompty\sethjuarez-unsalivated-sharan\runtime\python\prompty\.venv_new + +if not defined PROMPT set PROMPT=$P$G + +if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT% +if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME% + +set _OLD_VIRTUAL_PROMPT=%PROMPT% +set PROMPT=(.venv_new) %PROMPT% + +if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME% +set PYTHONHOME= + +if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH% +if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH% + +set PATH=%VIRTUAL_ENV%\Scripts;%PATH% +set VIRTUAL_ENV_PROMPT=(.venv_new) + +:END +if defined _OLD_CODEPAGE ( + "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul + set _OLD_CODEPAGE= +) diff --git a/runtime/python/prompty/.venv_new/Scripts/coverage-3.11.exe b/runtime/python/prompty/.venv_new/Scripts/coverage-3.11.exe new file mode 100644 index 00000000..eb52b0f5 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/coverage-3.11.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/coverage.exe b/runtime/python/prompty/.venv_new/Scripts/coverage.exe new file mode 100644 index 00000000..3d0c6d3e Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/coverage.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/coverage3.exe b/runtime/python/prompty/.venv_new/Scripts/coverage3.exe new file mode 100644 index 00000000..eb52b0f5 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/coverage3.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/deactivate.bat b/runtime/python/prompty/.venv_new/Scripts/deactivate.bat new file mode 100644 index 00000000..62a39a75 --- /dev/null +++ b/runtime/python/prompty/.venv_new/Scripts/deactivate.bat @@ -0,0 +1,22 @@ +@echo off + +if defined _OLD_VIRTUAL_PROMPT ( + set "PROMPT=%_OLD_VIRTUAL_PROMPT%" +) +set _OLD_VIRTUAL_PROMPT= + +if defined _OLD_VIRTUAL_PYTHONHOME ( + set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" + set _OLD_VIRTUAL_PYTHONHOME= +) + +if defined _OLD_VIRTUAL_PATH ( + set "PATH=%_OLD_VIRTUAL_PATH%" +) + +set _OLD_VIRTUAL_PATH= + +set VIRTUAL_ENV= +set VIRTUAL_ENV_PROMPT= + +:END diff --git a/runtime/python/prompty/.venv_new/Scripts/dotenv.exe b/runtime/python/prompty/.venv_new/Scripts/dotenv.exe new file mode 100644 index 00000000..9f220fd4 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/dotenv.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/pip.exe b/runtime/python/prompty/.venv_new/Scripts/pip.exe new file mode 100644 index 00000000..0ba195b2 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/pip.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/pip3.11.exe b/runtime/python/prompty/.venv_new/Scripts/pip3.11.exe new file mode 100644 index 00000000..0ba195b2 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/pip3.11.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/pip3.exe b/runtime/python/prompty/.venv_new/Scripts/pip3.exe new file mode 100644 index 00000000..0ba195b2 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/pip3.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/py.test.exe b/runtime/python/prompty/.venv_new/Scripts/py.test.exe new file mode 100644 index 00000000..df9ca5fe Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/py.test.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/pygmentize.exe b/runtime/python/prompty/.venv_new/Scripts/pygmentize.exe new file mode 100644 index 00000000..5f0a68de Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/pygmentize.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/pytest.exe b/runtime/python/prompty/.venv_new/Scripts/pytest.exe new file mode 100644 index 00000000..df9ca5fe Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/pytest.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/python.exe b/runtime/python/prompty/.venv_new/Scripts/python.exe new file mode 100644 index 00000000..f7cb1e38 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/python.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/pythonw.exe b/runtime/python/prompty/.venv_new/Scripts/pythonw.exe new file mode 100644 index 00000000..4fc3f909 Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/pythonw.exe differ diff --git a/runtime/python/prompty/.venv_new/Scripts/ruff.exe b/runtime/python/prompty/.venv_new/Scripts/ruff.exe new file mode 100644 index 00000000..731fc10e Binary files /dev/null and b/runtime/python/prompty/.venv_new/Scripts/ruff.exe differ diff --git a/runtime/python/prompty/.venv_new/pyvenv.cfg b/runtime/python/prompty/.venv_new/pyvenv.cfg new file mode 100644 index 00000000..10b3d69b --- /dev/null +++ b/runtime/python/prompty/.venv_new/pyvenv.cfg @@ -0,0 +1,5 @@ +home = C:\Users\sejuare\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0 +include-system-site-packages = false +version = 3.11.9 +executable = C:\Users\sejuare\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\python.exe +command = C:\Users\sejuare\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\python.exe -m venv C:\Users\sejuare\.copilot\copilot-worktrees\prompty\sethjuarez-unsalivated-sharan\runtime\python\prompty\.venv_new diff --git a/runtime/python/prompty/prompty/core/types.py b/runtime/python/prompty/prompty/core/types.py index 04c29762..b5ae4ee5 100644 --- a/runtime/python/prompty/prompty/core/types.py +++ b/runtime/python/prompty/prompty/core/types.py @@ -1,27 +1,46 @@ -"""Abstract message types for model-agnostic prompt representation. +"""Runtime extensions for the generated Message class. -The parser produces these types. Executors transform them into -provider-specific wire format (OpenAI, Anthropic, etc.). +The canonical type definitions (``Message``, ``ContentPart``, ``TextPart``, etc.) +and the ``MessageHelpers`` Protocol contract are generated by the TypeSpec +emitter and live in ``prompty.model``. This module re-exports them, provides +the Protocol-conforming ``text()``/``to_text_content()`` implementations, and +defines runtime-only streaming wrappers. + +The generated :class:`~prompty.model.MessageHelpers` Protocol is +``@runtime_checkable`` — a module-level ``isinstance`` assertion below +confirms the attached methods satisfy the contract. -Also provides streaming wrappers (``PromptyStream``, ``AsyncPromptyStream``) -that accumulate chunks and flush to the tracer on exhaustion. +The parser produces ``Message`` objects. Executors transform them into +provider-specific wire format (OpenAI, Anthropic, etc.). """ from __future__ import annotations from collections.abc import AsyncIterator, Iterator -from dataclasses import dataclass, field from typing import Any +# Re-export generated types — these are the canonical definitions. +from ..model import ( + AudioPart, + ContentPart, + FilePart, + ImagePart, + Message, + MessageHelpers, + TextPart, + ThreadMarker, +) + __all__ = [ "AsyncPromptyStream", + "AudioPart", "ContentPart", - "ImagePart", "FilePart", - "AudioPart", - "TextPart", + "ImagePart", "Message", + "MessageHelpers", "PromptyStream", + "TextPart", "ThreadMarker", "RICH_KINDS", "ROLES", @@ -35,124 +54,41 @@ # --------------------------------------------------------------------------- -# Content parts +# MessageHelpers implementation — attached to the generated Message class # --------------------------------------------------------------------------- -@dataclass -class ContentPart: - """Base class for typed content within a message. - - Each part has a ``kind`` discriminator and part-specific fields. - """ +def _message_text(self: Message) -> str: + """Concatenate all TextPart values into a single string.""" + return "".join(p.value for p in self.parts if isinstance(p, TextPart)) - kind: str +def _message_to_text_content(self: Message) -> str | list[dict[str, Any]]: + """Return plain string if all parts are text, else a parts list. -@dataclass -class TextPart(ContentPart): - """Plain text content.""" - - kind: str = field(default="text", init=False) - value: str = "" - - -@dataclass -class ImagePart(ContentPart): - """Image content — URL, data URI, or file path. - - Attributes: - source: URL, ``data:`` URI, or file path to the image. - detail: Processing detail level (``"auto"``, ``"low"``, ``"high"``). - media_type: MIME type (e.g. ``"image/png"``). Inferred if omitted. + This is useful for simple serialization — callers that only need + the text content can get a ``str``, while multimodal messages + get the full parts array. """ - - kind: str = field(default="image", init=False) - source: str = "" - detail: str | None = None - media_type: str | None = None - - -@dataclass -class FilePart(ContentPart): - """File/document attachment (PDF, etc.). - - Attributes: - source: URL or file path. - media_type: MIME type (e.g. ``"application/pdf"``). - """ - - kind: str = field(default="file", init=False) - source: str = "" - media_type: str | None = None - - -@dataclass -class AudioPart(ContentPart): - """Audio content. - - Attributes: - source: URL, ``data:`` URI, or file path. - media_type: MIME type (e.g. ``"audio/wav"``). - """ - - kind: str = field(default="audio", init=False) - source: str = "" - media_type: str | None = None - - -# --------------------------------------------------------------------------- -# Messages -# --------------------------------------------------------------------------- - - -@dataclass -class Message: - """A single message in the abstract message array. - - Attributes: - role: One of the recognized roles (system, user, assistant, developer). - parts: Ordered list of content parts. - metadata: Extra key-value pairs from role boundary args - (e.g. ``name``, custom attributes). - """ - - role: str - parts: list[ContentPart] = field(default_factory=list) - metadata: dict[str, Any] = field(default_factory=dict) - - @property - def text(self) -> str: - """Convenience: concatenate all TextPart values.""" - return "".join(p.value for p in self.parts if isinstance(p, TextPart)) - - def to_text_content(self) -> str | list[dict[str, Any]]: - """Return plain string if all parts are text, else a parts list. - - This is useful for simple serialization — callers that only need - the text content can get a ``str``, while multimodal messages - get the full parts array. - """ - if all(isinstance(p, TextPart) for p in self.parts): - return self.text - return [_part_to_dict(p) for p in self.parts] - - -@dataclass -class ThreadMarker: - """Positional marker for rich-kind input insertion. - - Emitted during prepare when the renderer nonce for a rich-kind input - (thread, image, file, audio) is found in parsed message text. - ``prepare()`` replaces these with actual content from the inputs. - - Attributes: - name: The input property name (e.g. ``"conversation"``). - kind: The rich kind (``"thread"``, ``"image"``, ``"file"``, ``"audio"``). - """ - - name: str = "thread" - kind: str = "thread" + if all(isinstance(p, TextPart) for p in self.parts): + return _message_text(self) + return [_part_to_dict(p) for p in self.parts] + + +# Bind the MessageHelpers contract methods to the generated class. The +# generated Protocol declares ``text`` as a ``@property`` (zero-param, +# non-verb name) and ``to_text_content()`` as a method (verb prefix ``to``), +# so we attach them accordingly. +Message.text = property(_message_text) # type: ignore[attr-defined] +Message.to_text_content = _message_to_text_content # type: ignore[attr-defined] + +# Verify the attached methods satisfy the generated Protocol contract. +# If this assertion ever fails, the emitter added a new @method declaration +# on Message that we have not implemented yet. +assert isinstance( + Message(role="user", parts=[], metadata={}), + MessageHelpers, +), "Message does not satisfy the generated MessageHelpers Protocol contract" # --------------------------------------------------------------------------- diff --git a/runtime/python/prompty/prompty/model/_Binding.py b/runtime/python/prompty/prompty/model/_Binding.py index 2b75bd0f..0e347fdf 100644 --- a/runtime/python/prompty/prompty/model/_Binding.py +++ b/runtime/python/prompty/prompty/model/_Binding.py @@ -43,7 +43,11 @@ def load(data: Any, context: LoadContext | None = None) -> "Binding": # handle alternate representations if isinstance(data, str): - data = {"input": data} + instance = Binding() + instance.input = data + if context is not None: + instance = context.process_output(instance) + return instance if not isinstance(data, dict): raise ValueError(f"Invalid data for Binding: {data}") diff --git a/runtime/python/prompty/prompty/model/_CompactionCompletePayload.py b/runtime/python/prompty/prompty/model/_CompactionCompletePayload.py new file mode 100644 index 00000000..7b385e3d --- /dev/null +++ b/runtime/python/prompty/prompty/model/_CompactionCompletePayload.py @@ -0,0 +1,104 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class CompactionCompletePayload: + """Payload for "compaction_complete" events — context compaction finished. + + Attributes + ---------- + removed : int + Number of messages removed during compaction + remaining : int + Number of messages remaining after compaction + """ + + _shorthand_property: ClassVar[str | None] = None + + removed: int = field(default=0) + remaining: int = field(default=0) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "CompactionCompletePayload": + """Load a CompactionCompletePayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + CompactionCompletePayload: The loaded CompactionCompletePayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for CompactionCompletePayload: {data}") + + # create new instance + instance = CompactionCompletePayload() + + if data is not None and "removed" in data: + instance.removed = data["removed"] + if data is not None and "remaining" in data: + instance.remaining = data["remaining"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the CompactionCompletePayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.removed is not None: + result["removed"] = obj.removed + if obj.remaining is not None: + result["remaining"] = obj.remaining + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the CompactionCompletePayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the CompactionCompletePayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_CompactionConfig.py b/runtime/python/prompty/prompty/model/_CompactionConfig.py new file mode 100644 index 00000000..ee62e127 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_CompactionConfig.py @@ -0,0 +1,113 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class CompactionConfig: + """Configuration for context window compaction. When the message history + exceeds the context budget, the compaction strategy is applied to + reduce the message list while preserving essential information. + + Attributes + ---------- + strategy : Optional[str] + The compaction strategy identifier. Built-in strategies include 'summarize'. Can also be a path to a .prompty file used as the summarization prompt. + budget : Optional[int] + Character budget for the compacted context. Overrides TurnOptions.contextBudget when set. + options : Optional[dict[str, Any]] + Additional strategy-specific options + """ + + _shorthand_property: ClassVar[str | None] = None + + strategy: str | None = None + budget: int | None = None + options: dict[str, Any] | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "CompactionConfig": + """Load a CompactionConfig instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + CompactionConfig: The loaded CompactionConfig instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for CompactionConfig: {data}") + + # create new instance + instance = CompactionConfig() + + if data is not None and "strategy" in data: + instance.strategy = data["strategy"] + if data is not None and "budget" in data: + instance.budget = data["budget"] + if data is not None and "options" in data: + instance.options = data["options"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the CompactionConfig instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.strategy is not None: + result["strategy"] = obj.strategy + if obj.budget is not None: + result["budget"] = obj.budget + if obj.options is not None: + result["options"] = obj.options + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the CompactionConfig instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the CompactionConfig instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_CompactionFailedPayload.py b/runtime/python/prompty/prompty/model/_CompactionFailedPayload.py new file mode 100644 index 00000000..efc5a758 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_CompactionFailedPayload.py @@ -0,0 +1,97 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class CompactionFailedPayload: + """Payload for "compaction_failed" events — compaction could not be completed. + + Attributes + ---------- + message : str + Explanation of why compaction failed + """ + + _shorthand_property: ClassVar[str | None] = None + + message: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "CompactionFailedPayload": + """Load a CompactionFailedPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + CompactionFailedPayload: The loaded CompactionFailedPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for CompactionFailedPayload: {data}") + + # create new instance + instance = CompactionFailedPayload() + + if data is not None and "message" in data: + instance.message = data["message"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the CompactionFailedPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.message is not None: + result["message"] = obj.message + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the CompactionFailedPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the CompactionFailedPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_Connection.py b/runtime/python/prompty/prompty/model/_Connection.py index 41d3c3c4..b2fb6272 100644 --- a/runtime/python/prompty/prompty/model/_Connection.py +++ b/runtime/python/prompty/prompty/model/_Connection.py @@ -21,17 +21,17 @@ class Connection(ABC): ---------- kind : str The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) - authenticationMode : Optional[str] + authentication_mode : Optional[str] The authority level for the connection, indicating under whose authority the connection is made (e.g., 'user', 'agent', 'system') - usageDescription : Optional[str] + usage_description : Optional[str] The usage description for the connection, providing context on how this connection will be used """ _shorthand_property: ClassVar[str | None] = None kind: str = field(default="") - authenticationMode: str | None = None - usageDescription: str | None = None + authentication_mode: str | None = None + usage_description: str | None = None @staticmethod def load(data: Any, context: LoadContext | None = None) -> "Connection": @@ -56,9 +56,9 @@ def load(data: Any, context: LoadContext | None = None) -> "Connection": if data is not None and "kind" in data: instance.kind = data["kind"] if data is not None and "authenticationMode" in data: - instance.authenticationMode = data["authenticationMode"] + instance.authentication_mode = data["authenticationMode"] if data is not None and "usageDescription" in data: - instance.usageDescription = data["usageDescription"] + instance.usage_description = data["usageDescription"] if context is not None: instance = context.process_output(instance) return instance @@ -76,10 +76,10 @@ def load_kind(data: dict, context: LoadContext | None) -> "Connection": return ApiKeyConnection.load(data, context) elif discriminator_value == "anonymous": return AnonymousConnection.load(data, context) - elif discriminator_value == "foundry": - return FoundryConnection.load(data, context) elif discriminator_value == "oauth": return OAuthConnection.load(data, context) + elif discriminator_value == "foundry": + return FoundryConnection.load(data, context) else: raise ValueError(f"Unknown Connection discriminator value: {discriminator_value}") @@ -102,10 +102,10 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: if obj.kind is not None: result["kind"] = obj.kind - if obj.authenticationMode is not None: - result["authenticationMode"] = obj.authenticationMode - if obj.usageDescription is not None: - result["usageDescription"] = obj.usageDescription + if obj.authentication_mode is not None: + result["authenticationMode"] = obj.authentication_mode + if obj.usage_description is not None: + result["usageDescription"] = obj.usage_description if context is not None: result = context.process_dict(result) @@ -208,7 +208,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["name"] = obj.name if obj.target is not None: result["target"] = obj.target - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -308,7 +307,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["name"] = obj.name if obj.endpoint is not None: result["endpoint"] = obj.endpoint - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -347,7 +345,7 @@ class ApiKeyConnection(Connection): The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) endpoint : str The endpoint URL for the AI service - apiKey : str + api_key : str The API key for authenticating with the AI service """ @@ -355,7 +353,7 @@ class ApiKeyConnection(Connection): kind: str = field(default="key") endpoint: str = field(default="") - apiKey: str = field(default="") + api_key: str = field(default="") @staticmethod def load(data: Any, context: LoadContext | None = None) -> "ApiKeyConnection": @@ -382,7 +380,7 @@ def load(data: Any, context: LoadContext | None = None) -> "ApiKeyConnection": if data is not None and "endpoint" in data: instance.endpoint = data["endpoint"] if data is not None and "apiKey" in data: - instance.apiKey = data["apiKey"] + instance.api_key = data["apiKey"] if context is not None: instance = context.process_output(instance) return instance @@ -406,9 +404,8 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.endpoint is not None: result["endpoint"] = obj.endpoint - if obj.apiKey is not None: - result["apiKey"] = obj.apiKey - + if obj.api_key is not None: + result["apiKey"] = obj.api_key return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -501,7 +498,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.endpoint is not None: result["endpoint"] = obj.endpoint - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -531,38 +527,44 @@ def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: @dataclass -class FoundryConnection(Connection): - """Connection configuration for Microsoft Foundry projects. - Provides project-scoped access to models, tools, and services - via Entra ID (DefaultAzureCredential) authentication. +class OAuthConnection(Connection): + """Connection configuration using OAuth 2.0 client credentials. + Useful for tools and services that require OAuth authentication, + such as MCP servers, OpenAPI endpoints, or other REST APIs. Attributes ---------- kind : str - The connection kind for Foundry project access + The connection kind for OAuth authentication endpoint : str - The Foundry project endpoint URL - name : Optional[str] - The named connection within the Foundry project - connectionType : Optional[str] - The connection type within the Foundry project (e.g., 'model', 'index', 'storage') + The endpoint URL for the service + client_id : str + The OAuth client ID + client_secret : str + The OAuth client secret + token_url : str + The OAuth token endpoint URL + scopes : Optional[list[str]] + OAuth scopes to request """ _shorthand_property: ClassVar[str | None] = None - kind: str = field(default="foundry") + kind: str = field(default="oauth") endpoint: str = field(default="") - name: str | None = None - connectionType: str | None = None + client_id: str = field(default="") + client_secret: str = field(default="") + token_url: str = field(default="") + scopes: list[str] = field(default_factory=list) @staticmethod - def load(data: Any, context: LoadContext | None = None) -> "FoundryConnection": - """Load a FoundryConnection instance. + def load(data: Any, context: LoadContext | None = None) -> "OAuthConnection": + """Load a OAuthConnection instance. Args: data (Any): The data to load the instance from. context (Optional[LoadContext]): Optional context with pre/post processing callbacks. Returns: - FoundryConnection: The loaded FoundryConnection instance. + OAuthConnection: The loaded OAuthConnection instance. """ @@ -570,25 +572,29 @@ def load(data: Any, context: LoadContext | None = None) -> "FoundryConnection": data = context.process_input(data) if not isinstance(data, dict): - raise ValueError(f"Invalid data for FoundryConnection: {data}") + raise ValueError(f"Invalid data for OAuthConnection: {data}") # create new instance - instance = FoundryConnection() + instance = OAuthConnection() if data is not None and "kind" in data: instance.kind = data["kind"] if data is not None and "endpoint" in data: instance.endpoint = data["endpoint"] - if data is not None and "name" in data: - instance.name = data["name"] - if data is not None and "connectionType" in data: - instance.connectionType = data["connectionType"] + if data is not None and "clientId" in data: + instance.client_id = data["clientId"] + if data is not None and "clientSecret" in data: + instance.client_secret = data["clientSecret"] + if data is not None and "tokenUrl" in data: + instance.token_url = data["tokenUrl"] + if data is not None and "scopes" in data: + instance.scopes = data["scopes"] if context is not None: instance = context.process_output(instance) return instance def save(self, context: SaveContext | None = None) -> dict[str, Any]: - """Save the FoundryConnection instance to a dictionary. + """Save the OAuthConnection instance to a dictionary. Args: context (Optional[SaveContext]): Optional context with pre/post processing callbacks. Returns: @@ -606,15 +612,18 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.endpoint is not None: result["endpoint"] = obj.endpoint - if obj.name is not None: - result["name"] = obj.name - if obj.connectionType is not None: - result["connectionType"] = obj.connectionType - + if obj.client_id is not None: + result["clientId"] = obj.client_id + if obj.client_secret is not None: + result["clientSecret"] = obj.client_secret + if obj.token_url is not None: + result["tokenUrl"] = obj.token_url + if obj.scopes is not None: + result["scopes"] = obj.scopes return result def to_yaml(self, context: SaveContext | None = None) -> str: - """Convert the FoundryConnection instance to a YAML string. + """Convert the OAuthConnection instance to a YAML string. Args: context (Optional[SaveContext]): Optional context with pre/post processing callbacks. Returns: @@ -626,7 +635,7 @@ def to_yaml(self, context: SaveContext | None = None) -> str: return context.to_yaml(self.save(context)) def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: - """Convert the FoundryConnection instance to a JSON string. + """Convert the OAuthConnection instance to a JSON string. Args: context (Optional[SaveContext]): Optional context with pre/post processing callbacks. indent (int): Number of spaces for indentation. Defaults to 2. @@ -640,44 +649,38 @@ def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: @dataclass -class OAuthConnection(Connection): - """Connection configuration using OAuth 2.0 client credentials. - Useful for tools and services that require OAuth authentication, - such as MCP servers, OpenAPI endpoints, or other REST APIs. +class FoundryConnection(Connection): + """Connection configuration for Microsoft Foundry projects. + Provides project-scoped access to models, tools, and services + via Entra ID (DefaultAzureCredential) authentication. Attributes ---------- kind : str - The connection kind for OAuth authentication + The connection kind for Foundry project access endpoint : str - The endpoint URL for the service - clientId : str - The OAuth client ID - clientSecret : str - The OAuth client secret - tokenUrl : str - The OAuth token endpoint URL - scopes : list[str] - OAuth scopes to request + The Foundry project endpoint URL + name : Optional[str] + The named connection within the Foundry project + connection_type : Optional[str] + The connection type within the Foundry project (e.g., 'model', 'index', 'storage') """ _shorthand_property: ClassVar[str | None] = None - kind: str = field(default="oauth") + kind: str = field(default="foundry") endpoint: str = field(default="") - clientId: str = field(default="") - clientSecret: str = field(default="") - tokenUrl: str = field(default="") - scopes: list[str] = field(default_factory=list) + name: str | None = None + connection_type: str | None = None @staticmethod - def load(data: Any, context: LoadContext | None = None) -> "OAuthConnection": - """Load a OAuthConnection instance. + def load(data: Any, context: LoadContext | None = None) -> "FoundryConnection": + """Load a FoundryConnection instance. Args: data (Any): The data to load the instance from. context (Optional[LoadContext]): Optional context with pre/post processing callbacks. Returns: - OAuthConnection: The loaded OAuthConnection instance. + FoundryConnection: The loaded FoundryConnection instance. """ @@ -685,29 +688,25 @@ def load(data: Any, context: LoadContext | None = None) -> "OAuthConnection": data = context.process_input(data) if not isinstance(data, dict): - raise ValueError(f"Invalid data for OAuthConnection: {data}") + raise ValueError(f"Invalid data for FoundryConnection: {data}") # create new instance - instance = OAuthConnection() + instance = FoundryConnection() if data is not None and "kind" in data: instance.kind = data["kind"] if data is not None and "endpoint" in data: instance.endpoint = data["endpoint"] - if data is not None and "clientId" in data: - instance.clientId = data["clientId"] - if data is not None and "clientSecret" in data: - instance.clientSecret = data["clientSecret"] - if data is not None and "tokenUrl" in data: - instance.tokenUrl = data["tokenUrl"] - if data is not None and "scopes" in data: - instance.scopes = data["scopes"] + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "connectionType" in data: + instance.connection_type = data["connectionType"] if context is not None: instance = context.process_output(instance) return instance def save(self, context: SaveContext | None = None) -> dict[str, Any]: - """Save the OAuthConnection instance to a dictionary. + """Save the FoundryConnection instance to a dictionary. Args: context (Optional[SaveContext]): Optional context with pre/post processing callbacks. Returns: @@ -725,19 +724,14 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.endpoint is not None: result["endpoint"] = obj.endpoint - if obj.clientId is not None: - result["clientId"] = obj.clientId - if obj.clientSecret is not None: - result["clientSecret"] = obj.clientSecret - if obj.tokenUrl is not None: - result["tokenUrl"] = obj.tokenUrl - if obj.scopes is not None: - result["scopes"] = obj.scopes - + if obj.name is not None: + result["name"] = obj.name + if obj.connection_type is not None: + result["connectionType"] = obj.connection_type return result def to_yaml(self, context: SaveContext | None = None) -> str: - """Convert the OAuthConnection instance to a YAML string. + """Convert the FoundryConnection instance to a YAML string. Args: context (Optional[SaveContext]): Optional context with pre/post processing callbacks. Returns: @@ -749,7 +743,7 @@ def to_yaml(self, context: SaveContext | None = None) -> str: return context.to_yaml(self.save(context)) def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: - """Convert the OAuthConnection instance to a JSON string. + """Convert the FoundryConnection instance to a JSON string. Args: context (Optional[SaveContext]): Optional context with pre/post processing callbacks. indent (int): Number of spaces for indentation. Defaults to 2. diff --git a/runtime/python/prompty/prompty/model/_ContentPart.py b/runtime/python/prompty/prompty/model/_ContentPart.py new file mode 100644 index 00000000..19ebd7f4 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ContentPart.py @@ -0,0 +1,514 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from abc import ABC +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ContentPart(ABC): + """A part of a message's content. Content parts are discriminated on the `kind` + field and represent the different modalities that can appear in a message. + + Attributes + ---------- + kind : str + The kind of content part + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ContentPart": + """Load a ContentPart instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ContentPart: The loaded ContentPart instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ContentPart: {data}") + + # load polymorphic ContentPart instance + instance = ContentPart.load_kind(data, context) + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_kind(data: dict, context: LoadContext | None) -> "ContentPart": + # load polymorphic ContentPart instance + if data is not None and "kind" in data: + discriminator_value = str(data["kind"]).lower() + if discriminator_value == "text": + return TextPart.load(data, context) + elif discriminator_value == "image": + return ImagePart.load(data, context) + elif discriminator_value == "file": + return FilePart.load(data, context) + elif discriminator_value == "audio": + return AudioPart.load(data, context) + + else: + raise ValueError(f"Unknown ContentPart discriminator value: {discriminator_value}") + else: + raise ValueError("Missing ContentPart discriminator property: 'kind'") + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ContentPart instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.kind is not None: + result["kind"] = obj.kind + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ContentPart instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ContentPart instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class TextPart(ContentPart): + """A text content part. + + Attributes + ---------- + kind : str + The kind identifier for text content + value : str + The text content + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="text") + value: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "TextPart": + """Load a TextPart instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + TextPart: The loaded TextPart instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for TextPart: {data}") + + # create new instance + instance = TextPart() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "value" in data: + instance.value = data["value"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the TextPart instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.value is not None: + result["value"] = obj.value + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the TextPart instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the TextPart instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class ImagePart(ContentPart): + """An image content part. The source may be a URL or base64-encoded data. + + Attributes + ---------- + kind : str + The kind identifier for image content + source : str + URL or base64-encoded image data + detail : Optional[str] + Detail level hint for the model (e.g., 'auto', 'low', 'high') + media_type : Optional[str] + MIME type of the image (e.g., 'image/png') + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="image") + source: str = field(default="") + detail: str | None = None + media_type: str | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ImagePart": + """Load a ImagePart instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ImagePart: The loaded ImagePart instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ImagePart: {data}") + + # create new instance + instance = ImagePart() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "source" in data: + instance.source = data["source"] + if data is not None and "detail" in data: + instance.detail = data["detail"] + if data is not None and "mediaType" in data: + instance.media_type = data["mediaType"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ImagePart instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.source is not None: + result["source"] = obj.source + if obj.detail is not None: + result["detail"] = obj.detail + if obj.media_type is not None: + result["mediaType"] = obj.media_type + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ImagePart instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ImagePart instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class FilePart(ContentPart): + """A file content part. The source may be a URL or base64-encoded data. + + Attributes + ---------- + kind : str + The kind identifier for file content + source : str + URL or base64-encoded file data + media_type : Optional[str] + MIME type of the file (e.g., 'application/pdf') + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="file") + source: str = field(default="") + media_type: str | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "FilePart": + """Load a FilePart instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + FilePart: The loaded FilePart instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for FilePart: {data}") + + # create new instance + instance = FilePart() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "source" in data: + instance.source = data["source"] + if data is not None and "mediaType" in data: + instance.media_type = data["mediaType"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the FilePart instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.source is not None: + result["source"] = obj.source + if obj.media_type is not None: + result["mediaType"] = obj.media_type + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the FilePart instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the FilePart instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class AudioPart(ContentPart): + """An audio content part. The source may be a URL or base64-encoded data. + + Attributes + ---------- + kind : str + The kind identifier for audio content + source : str + URL or base64-encoded audio data + media_type : Optional[str] + MIME type of the audio (e.g., 'audio/wav') + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="audio") + source: str = field(default="") + media_type: str | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "AudioPart": + """Load a AudioPart instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + AudioPart: The loaded AudioPart instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for AudioPart: {data}") + + # create new instance + instance = AudioPart() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "source" in data: + instance.source = data["source"] + if data is not None and "mediaType" in data: + instance.media_type = data["mediaType"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the AudioPart instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.source is not None: + result["source"] = obj.source + if obj.media_type is not None: + result["mediaType"] = obj.media_type + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the AudioPart instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the AudioPart instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_DoneEventPayload.py b/runtime/python/prompty/prompty/model/_DoneEventPayload.py new file mode 100644 index 00000000..bb5dce79 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_DoneEventPayload.py @@ -0,0 +1,128 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext +from ._Message import Message + + +@dataclass +class DoneEventPayload: + """Payload for "done" events — the agent loop completed successfully. + + Attributes + ---------- + response : str + The final text response from the LLM + messages : list[Message] + The final conversation state including all messages + """ + + _shorthand_property: ClassVar[str | None] = None + + response: str = field(default="") + messages: list[Message] = field(default_factory=list) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "DoneEventPayload": + """Load a DoneEventPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + DoneEventPayload: The loaded DoneEventPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for DoneEventPayload: {data}") + + # create new instance + instance = DoneEventPayload() + + if data is not None and "response" in data: + instance.response = data["response"] + if data is not None and "messages" in data: + instance.messages = DoneEventPayload.load_messages(data["messages"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_messages(data: dict | list, context: LoadContext | None) -> list[Message]: + if isinstance(data, dict): + # convert simple named messages to list of Message + result = [] + for k, v in data.items(): + if isinstance(v, dict): + # value is an object, spread its properties + result.append({"name": k, **v}) + else: + # value is a scalar, use it as the primary property + result.append({"name": k, "role": v}) + data = result + return [Message.load(item, context) for item in data] + + @staticmethod + def save_messages(items: list[Message], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]: + if context is None: + context = SaveContext() + + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the DoneEventPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.response is not None: + result["response"] = obj.response + if obj.messages is not None: + result["messages"] = DoneEventPayload.save_messages(obj.messages, context) + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the DoneEventPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the DoneEventPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ErrorEventPayload.py b/runtime/python/prompty/prompty/model/_ErrorEventPayload.py new file mode 100644 index 00000000..75fb522b --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ErrorEventPayload.py @@ -0,0 +1,97 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ErrorEventPayload: + """Payload for "error" events — an error occurred during the loop. + + Attributes + ---------- + message : str + Human-readable error description + """ + + _shorthand_property: ClassVar[str | None] = None + + message: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ErrorEventPayload": + """Load a ErrorEventPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ErrorEventPayload: The loaded ErrorEventPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ErrorEventPayload: {data}") + + # create new instance + instance = ErrorEventPayload() + + if data is not None and "message" in data: + instance.message = data["message"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ErrorEventPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.message is not None: + result["message"] = obj.message + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ErrorEventPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ErrorEventPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_Executor.py b/runtime/python/prompty/prompty/model/_Executor.py new file mode 100644 index 00000000..a9de64c6 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_Executor.py @@ -0,0 +1,38 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from typing import Any, Protocol, runtime_checkable + +from ._Message import Message +from ._Prompty import Prompty +from ._ToolCall import ToolCall + + +@runtime_checkable +class Executor(Protocol): + """Calls an LLM provider with messages and returns the raw provider response.""" + + def execute(self, agent: Prompty, messages: list[Message]) -> Any: + """Call an LLM provider with messages and return the raw response""" + ... + + async def execute_async(self, agent: Prompty, messages: list[Message]) -> Any: + """Call an LLM provider with messages and return the raw response (async variant)""" + ... + + def execute_stream(self, agent: Prompty, messages: list[Message]) -> Any: + """Call an LLM provider and return a streaming response. Returns a language-specific async iterable/stream of raw chunks. Not all providers support streaming; the default implementation should signal lack of support.""" + return None + + async def execute_stream_async(self, agent: Prompty, messages: list[Message]) -> Any: + """Call an LLM provider and return a streaming response. Returns a language-specific async iterable/stream of raw chunks. Not all providers support streaming; the default implementation should signal lack of support. (async variant)""" + return None + + def format_tool_messages( + self, raw_response: Any, tool_calls: list[ToolCall], tool_results: list[str], text_content: str | None + ) -> list[Message]: + """Format tool call results into messages for the next iteration""" + ... diff --git a/runtime/python/prompty/prompty/model/_FormatConfig.py b/runtime/python/prompty/prompty/model/_FormatConfig.py index ace13c23..23d340ab 100644 --- a/runtime/python/prompty/prompty/model/_FormatConfig.py +++ b/runtime/python/prompty/prompty/model/_FormatConfig.py @@ -46,7 +46,11 @@ def load(data: Any, context: LoadContext | None = None) -> "FormatConfig": # handle alternate representations if isinstance(data, str): - data = {"kind": data} + instance = FormatConfig() + instance.kind = data + if context is not None: + instance = context.process_output(instance) + return instance if not isinstance(data, dict): raise ValueError(f"Invalid data for FormatConfig: {data}") diff --git a/runtime/python/prompty/prompty/model/_GuardrailResult.py b/runtime/python/prompty/prompty/model/_GuardrailResult.py new file mode 100644 index 00000000..843c0b15 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_GuardrailResult.py @@ -0,0 +1,128 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class GuardrailResult: + """The result of a guardrail evaluation. Guardrails are safety checks that + run at specific phases of the agent loop and can allow, deny, or rewrite + content. + + Attributes + ---------- + allowed : bool + Whether the content passed the guardrail check + reason : Optional[str] + Explanation of why the content was allowed or denied + rewrite : Optional[Any] + Optional rewritten content to replace the original + """ + + _shorthand_property: ClassVar[str | None] = None + + allowed: bool = field(default=False) + reason: str | None = None + rewrite: Any | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "GuardrailResult": + """Load a GuardrailResult instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + GuardrailResult: The loaded GuardrailResult instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for GuardrailResult: {data}") + + # create new instance + instance = GuardrailResult() + + if data is not None and "allowed" in data: + instance.allowed = data["allowed"] + if data is not None and "reason" in data: + instance.reason = data["reason"] + if data is not None and "rewrite" in data: + instance.rewrite = data["rewrite"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the GuardrailResult instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.allowed is not None: + result["allowed"] = obj.allowed + if obj.reason is not None: + result["reason"] = obj.reason + if obj.rewrite is not None: + result["rewrite"] = obj.rewrite + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the GuardrailResult instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the GuardrailResult instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + @classmethod + def create_rewrite(cls, rewrite: Any) -> "GuardrailResult": + """Create a GuardrailResult with preset field values.""" + return GuardrailResult(allowed=True, rewrite=rewrite) + + @classmethod + def deny(cls, reason: str) -> "GuardrailResult": + """Create a GuardrailResult with preset field values.""" + return GuardrailResult(allowed=False, reason=reason) + + @classmethod + def allow(cls) -> "GuardrailResult": + """Create a GuardrailResult with preset field values.""" + return GuardrailResult(allowed=True) diff --git a/runtime/python/prompty/prompty/model/_McpApprovalMode.py b/runtime/python/prompty/prompty/model/_McpApprovalMode.py index 52fd9b34..b4d9a126 100644 --- a/runtime/python/prompty/prompty/model/_McpApprovalMode.py +++ b/runtime/python/prompty/prompty/model/_McpApprovalMode.py @@ -20,17 +20,17 @@ class McpApprovalMode: ---------- kind : str The approval mode: 'always', 'never', or 'specify' - alwaysRequireApprovalTools : list[str] + always_require_approval_tools : Optional[list[str]] List of tools that always require approval (only used when kind is 'specify') - neverRequireApprovalTools : list[str] + never_require_approval_tools : Optional[list[str]] List of tools that never require approval (only used when kind is 'specify') """ _shorthand_property: ClassVar[str | None] = "kind" kind: str = field(default="") - alwaysRequireApprovalTools: list[str] = field(default_factory=list) - neverRequireApprovalTools: list[str] = field(default_factory=list) + always_require_approval_tools: list[str] = field(default_factory=list) + never_require_approval_tools: list[str] = field(default_factory=list) @staticmethod def load(data: Any, context: LoadContext | None = None) -> "McpApprovalMode": @@ -48,7 +48,11 @@ def load(data: Any, context: LoadContext | None = None) -> "McpApprovalMode": # handle alternate representations if isinstance(data, str): - data = {"kind": data} + instance = McpApprovalMode() + instance.kind = data + if context is not None: + instance = context.process_output(instance) + return instance if not isinstance(data, dict): raise ValueError(f"Invalid data for McpApprovalMode: {data}") @@ -59,9 +63,9 @@ def load(data: Any, context: LoadContext | None = None) -> "McpApprovalMode": if data is not None and "kind" in data: instance.kind = data["kind"] if data is not None and "alwaysRequireApprovalTools" in data: - instance.alwaysRequireApprovalTools = data["alwaysRequireApprovalTools"] + instance.always_require_approval_tools = data["alwaysRequireApprovalTools"] if data is not None and "neverRequireApprovalTools" in data: - instance.neverRequireApprovalTools = data["neverRequireApprovalTools"] + instance.never_require_approval_tools = data["neverRequireApprovalTools"] if context is not None: instance = context.process_output(instance) return instance @@ -82,10 +86,10 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: if obj.kind is not None: result["kind"] = obj.kind - if obj.alwaysRequireApprovalTools is not None: - result["alwaysRequireApprovalTools"] = obj.alwaysRequireApprovalTools - if obj.neverRequireApprovalTools is not None: - result["neverRequireApprovalTools"] = obj.neverRequireApprovalTools + if obj.always_require_approval_tools is not None: + result["alwaysRequireApprovalTools"] = obj.always_require_approval_tools + if obj.never_require_approval_tools is not None: + result["neverRequireApprovalTools"] = obj.never_require_approval_tools if context is not None: result = context.process_dict(result) diff --git a/runtime/python/prompty/prompty/model/_Message.py b/runtime/python/prompty/prompty/model/_Message.py new file mode 100644 index 00000000..7b66afef --- /dev/null +++ b/runtime/python/prompty/prompty/model/_Message.py @@ -0,0 +1,171 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar, Protocol, runtime_checkable + +from ._ContentPart import ContentPart, TextPart +from ._context import LoadContext, SaveContext + + +@dataclass +class Message: + """A message in a conversation. Messages have a role and a list of content parts + representing the different modalities of the message content. + + Attributes + ---------- + role : str + The role of the message sender + parts : list[ContentPart] + The content parts of the message + metadata : dict[str, Any] + Optional metadata associated with the message + """ + + _shorthand_property: ClassVar[str | None] = None + + role: str = field(default="user") + parts: list[ContentPart] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "Message": + """Load a Message instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + Message: The loaded Message instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for Message: {data}") + + # create new instance + instance = Message() + + if data is not None and "role" in data: + instance.role = data["role"] + if data is not None and "parts" in data: + instance.parts = Message.load_parts(data["parts"], context) + if data is not None and "metadata" in data: + instance.metadata = data["metadata"] + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_parts(data: dict | list, context: LoadContext | None) -> list[ContentPart]: + if isinstance(data, dict): + # convert simple named parts to list of ContentPart + result = [] + for k, v in data.items(): + if isinstance(v, dict): + # value is an object, spread its properties + result.append({"name": k, **v}) + else: + # value is a scalar, use it as the primary property + result.append({"name": k, "kind": v}) + data = result + return [ContentPart.load(item, context) for item in data] + + @staticmethod + def save_parts(items: list[ContentPart], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]: + if context is None: + context = SaveContext() + + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the Message instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.role is not None: + result["role"] = obj.role + if obj.parts is not None: + result["parts"] = Message.save_parts(obj.parts, context) + if obj.metadata is not None: + result["metadata"] = obj.metadata + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the Message instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the Message instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + @classmethod + def assistant(cls, text: str) -> "Message": + """Create a Message with preset field values.""" + return Message(role="assistant", parts=[TextPart(value=text)]) + + @classmethod + def system(cls, text: str) -> "Message": + """Create a Message with preset field values.""" + return Message(role="system", parts=[TextPart(value=text)]) + + @classmethod + def user(cls, text: str) -> "Message": + """Create a Message with preset field values.""" + return Message(role="user", parts=[TextPart(value=text)]) + + +@runtime_checkable +class MessageHelpers(Protocol): + """Helper contract for Message. + + Runtime implementations must provide these methods on every Message + instance (either by attaching them to the generated class or by wrapping it). + The type checker can verify conformance by annotating against this Protocol + or by calling isinstance(instance, MessageHelpers) at runtime. + """ + + def to_text_content(self) -> Any: + """Return plain string if all parts are text, else a list of content part dicts for wire serialization""" + ... + + @property + def text(self) -> str: + """Concatenate all TextPart values joined by newline""" + ... diff --git a/runtime/python/prompty/prompty/model/_MessagesUpdatedPayload.py b/runtime/python/prompty/prompty/model/_MessagesUpdatedPayload.py new file mode 100644 index 00000000..cec2c32a --- /dev/null +++ b/runtime/python/prompty/prompty/model/_MessagesUpdatedPayload.py @@ -0,0 +1,121 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext +from ._Message import Message + + +@dataclass +class MessagesUpdatedPayload: + """Payload for "messages_updated" events — the conversation state has changed. + + Attributes + ---------- + messages : list[Message] + The current full message list after the update + """ + + _shorthand_property: ClassVar[str | None] = None + + messages: list[Message] = field(default_factory=list) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "MessagesUpdatedPayload": + """Load a MessagesUpdatedPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + MessagesUpdatedPayload: The loaded MessagesUpdatedPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for MessagesUpdatedPayload: {data}") + + # create new instance + instance = MessagesUpdatedPayload() + + if data is not None and "messages" in data: + instance.messages = MessagesUpdatedPayload.load_messages(data["messages"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_messages(data: dict | list, context: LoadContext | None) -> list[Message]: + if isinstance(data, dict): + # convert simple named messages to list of Message + result = [] + for k, v in data.items(): + if isinstance(v, dict): + # value is an object, spread its properties + result.append({"name": k, **v}) + else: + # value is a scalar, use it as the primary property + result.append({"name": k, "role": v}) + data = result + return [Message.load(item, context) for item in data] + + @staticmethod + def save_messages(items: list[Message], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]: + if context is None: + context = SaveContext() + + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the MessagesUpdatedPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.messages is not None: + result["messages"] = MessagesUpdatedPayload.save_messages(obj.messages, context) + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the MessagesUpdatedPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the MessagesUpdatedPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_Model.py b/runtime/python/prompty/prompty/model/_Model.py index d1f57eef..23bda843 100644 --- a/runtime/python/prompty/prompty/model/_Model.py +++ b/runtime/python/prompty/prompty/model/_Model.py @@ -24,7 +24,7 @@ class Model: The unique identifier of the model - can be used as the single property shorthand provider : Optional[str] The provider of the model (e.g., 'openai', 'foundry', 'anthropic') - apiType : Optional[str] + api_type : Optional[str] The type of API to use for the model (e.g., 'chat', 'response', etc.) connection : Optional[Connection] The connection configuration for the model @@ -36,7 +36,7 @@ class Model: id: str = field(default="") provider: str | None = None - apiType: str | None = None + api_type: str | None = None connection: Connection | None = None options: ModelOptions | None = None @@ -56,7 +56,11 @@ def load(data: Any, context: LoadContext | None = None) -> "Model": # handle alternate representations if isinstance(data, str): - data = {"id": data} + instance = Model() + instance.id = data + if context is not None: + instance = context.process_output(instance) + return instance if not isinstance(data, dict): raise ValueError(f"Invalid data for Model: {data}") @@ -69,7 +73,7 @@ def load(data: Any, context: LoadContext | None = None) -> "Model": if data is not None and "provider" in data: instance.provider = data["provider"] if data is not None and "apiType" in data: - instance.apiType = data["apiType"] + instance.api_type = data["apiType"] if data is not None and "connection" in data: instance.connection = Connection.load(data["connection"], context) if data is not None and "options" in data: @@ -96,8 +100,8 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["id"] = obj.id if obj.provider is not None: result["provider"] = obj.provider - if obj.apiType is not None: - result["apiType"] = obj.apiType + if obj.api_type is not None: + result["apiType"] = obj.api_type if obj.connection is not None: result["connection"] = obj.connection.save(context) if obj.options is not None: diff --git a/runtime/python/prompty/prompty/model/_ModelInfo.py b/runtime/python/prompty/prompty/model/_ModelInfo.py new file mode 100644 index 00000000..9f03270b --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ModelInfo.py @@ -0,0 +1,144 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ModelInfo: + """Information about a model available from a provider. Used by provider-level + model discovery to report which models are available and their capabilities. + + Not all providers return all fields — implementations SHOULD populate as + many fields as the provider's API supports and MAY enrich sparse results + from a built-in lookup table of known models. + + Attributes + ---------- + id : str + The model identifier (e.g., 'gpt-4o', 'claude-3-opus') + display_name : Optional[str] + Human-readable display name + owned_by : Optional[str] + The organization or entity that owns the model + context_window : Optional[int] + Maximum context window size in tokens + input_modalities : Optional[list[str]] + Input modalities the model accepts (e.g., 'text', 'image', 'audio') + output_modalities : Optional[list[str]] + Output modalities the model can produce (e.g., 'text', 'audio') + additional_properties : Optional[dict[str, Any]] + Additional provider-specific properties + """ + + _shorthand_property: ClassVar[str | None] = None + + id: str = field(default="") + display_name: str | None = None + owned_by: str | None = None + context_window: int | None = None + input_modalities: list[str] = field(default_factory=list) + output_modalities: list[str] = field(default_factory=list) + additional_properties: dict[str, Any] | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ModelInfo": + """Load a ModelInfo instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ModelInfo: The loaded ModelInfo instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ModelInfo: {data}") + + # create new instance + instance = ModelInfo() + + if data is not None and "id" in data: + instance.id = data["id"] + if data is not None and "displayName" in data: + instance.display_name = data["displayName"] + if data is not None and "ownedBy" in data: + instance.owned_by = data["ownedBy"] + if data is not None and "contextWindow" in data: + instance.context_window = data["contextWindow"] + if data is not None and "inputModalities" in data: + instance.input_modalities = data["inputModalities"] + if data is not None and "outputModalities" in data: + instance.output_modalities = data["outputModalities"] + if data is not None and "additionalProperties" in data: + instance.additional_properties = data["additionalProperties"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ModelInfo instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.id is not None: + result["id"] = obj.id + if obj.display_name is not None: + result["displayName"] = obj.display_name + if obj.owned_by is not None: + result["ownedBy"] = obj.owned_by + if obj.context_window is not None: + result["contextWindow"] = obj.context_window + if obj.input_modalities is not None: + result["inputModalities"] = obj.input_modalities + if obj.output_modalities is not None: + result["outputModalities"] = obj.output_modalities + if obj.additional_properties is not None: + result["additionalProperties"] = obj.additional_properties + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ModelInfo instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ModelInfo instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ModelOptions.py b/runtime/python/prompty/prompty/model/_ModelOptions.py index c1c23fbf..9fdd9329 100644 --- a/runtime/python/prompty/prompty/model/_ModelOptions.py +++ b/runtime/python/prompty/prompty/model/_ModelOptions.py @@ -16,40 +16,40 @@ class ModelOptions: Attributes ---------- - frequencyPenalty : Optional[float] + frequency_penalty : Optional[float] The frequency penalty to apply to the model's output - maxOutputTokens : Optional[int] + max_output_tokens : Optional[int] The maximum number of tokens to generate in the output - presencePenalty : Optional[float] + presence_penalty : Optional[float] The presence penalty to apply to the model's output seed : Optional[int] A random seed for deterministic output temperature : Optional[float] The temperature to use for sampling - topK : Optional[int] + top_k : Optional[int] The top-K sampling value - topP : Optional[float] + top_p : Optional[float] The top-P sampling value - stopSequences : list[str] + stop_sequences : Optional[list[str]] Stop sequences to end generation - allowMultipleToolCalls : Optional[bool] + allow_multiple_tool_calls : Optional[bool] Whether to allow multiple tool calls in a single response - additionalProperties : Optional[dict[str, Any]] + additional_properties : Optional[dict[str, Any]] Additional custom properties for model options """ _shorthand_property: ClassVar[str | None] = None - frequencyPenalty: float | None = None - maxOutputTokens: int | None = None - presencePenalty: float | None = None + frequency_penalty: float | None = None + max_output_tokens: int | None = None + presence_penalty: float | None = None seed: int | None = None temperature: float | None = None - topK: int | None = None - topP: float | None = None - stopSequences: list[str] = field(default_factory=list) - allowMultipleToolCalls: bool | None = None - additionalProperties: dict[str, Any] | None = None + top_k: int | None = None + top_p: float | None = None + stop_sequences: list[str] = field(default_factory=list) + allow_multiple_tool_calls: bool | None = None + additional_properties: dict[str, Any] | None = None @staticmethod def load(data: Any, context: LoadContext | None = None) -> "ModelOptions": @@ -72,25 +72,25 @@ def load(data: Any, context: LoadContext | None = None) -> "ModelOptions": instance = ModelOptions() if data is not None and "frequencyPenalty" in data: - instance.frequencyPenalty = data["frequencyPenalty"] + instance.frequency_penalty = data["frequencyPenalty"] if data is not None and "maxOutputTokens" in data: - instance.maxOutputTokens = data["maxOutputTokens"] + instance.max_output_tokens = data["maxOutputTokens"] if data is not None and "presencePenalty" in data: - instance.presencePenalty = data["presencePenalty"] + instance.presence_penalty = data["presencePenalty"] if data is not None and "seed" in data: instance.seed = data["seed"] if data is not None and "temperature" in data: instance.temperature = data["temperature"] if data is not None and "topK" in data: - instance.topK = data["topK"] + instance.top_k = data["topK"] if data is not None and "topP" in data: - instance.topP = data["topP"] + instance.top_p = data["topP"] if data is not None and "stopSequences" in data: - instance.stopSequences = data["stopSequences"] + instance.stop_sequences = data["stopSequences"] if data is not None and "allowMultipleToolCalls" in data: - instance.allowMultipleToolCalls = data["allowMultipleToolCalls"] + instance.allow_multiple_tool_calls = data["allowMultipleToolCalls"] if data is not None and "additionalProperties" in data: - instance.additionalProperties = data["additionalProperties"] + instance.additional_properties = data["additionalProperties"] if context is not None: instance = context.process_output(instance) return instance @@ -109,31 +109,62 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result: dict[str, Any] = {} - if obj.frequencyPenalty is not None: - result["frequencyPenalty"] = obj.frequencyPenalty - if obj.maxOutputTokens is not None: - result["maxOutputTokens"] = obj.maxOutputTokens - if obj.presencePenalty is not None: - result["presencePenalty"] = obj.presencePenalty + if obj.frequency_penalty is not None: + result["frequencyPenalty"] = obj.frequency_penalty + if obj.max_output_tokens is not None: + result["maxOutputTokens"] = obj.max_output_tokens + if obj.presence_penalty is not None: + result["presencePenalty"] = obj.presence_penalty if obj.seed is not None: result["seed"] = obj.seed if obj.temperature is not None: result["temperature"] = obj.temperature - if obj.topK is not None: - result["topK"] = obj.topK - if obj.topP is not None: - result["topP"] = obj.topP - if obj.stopSequences is not None: - result["stopSequences"] = obj.stopSequences - if obj.allowMultipleToolCalls is not None: - result["allowMultipleToolCalls"] = obj.allowMultipleToolCalls - if obj.additionalProperties is not None: - result["additionalProperties"] = obj.additionalProperties + if obj.top_k is not None: + result["topK"] = obj.top_k + if obj.top_p is not None: + result["topP"] = obj.top_p + if obj.stop_sequences is not None: + result["stopSequences"] = obj.stop_sequences + if obj.allow_multiple_tool_calls is not None: + result["allowMultipleToolCalls"] = obj.allow_multiple_tool_calls + if obj.additional_properties is not None: + result["additionalProperties"] = obj.additional_properties if context is not None: result = context.process_dict(result) return result + def to_wire(self, provider: str) -> dict[str, Any]: + """Convert to provider-specific wire format. + Args: + provider (str): The provider to convert to (e.g., "openai", "anthropic"). + Returns: + dict[str, Any]: The wire-format dictionary with provider-specific field names. + + """ + data = self.save() + result: dict[str, Any] = {} + wire_map: dict[str, dict[str, str]] = { + "frequencyPenalty": {"openai": "frequency_penalty"}, + "maxOutputTokens": { + "openai": "max_completion_tokens", + "responses": "max_output_tokens", + "anthropic": "max_tokens", + }, + "presencePenalty": {"openai": "presence_penalty"}, + "seed": {"openai": "seed"}, + "temperature": {"openai": "temperature", "responses": "temperature", "anthropic": "temperature"}, + "topK": {"openai": "top_k", "anthropic": "top_k"}, + "topP": {"openai": "top_p", "responses": "top_p", "anthropic": "top_p"}, + "stopSequences": {"openai": "stop", "anthropic": "stop_sequences"}, + "allowMultipleToolCalls": {"openai": "parallel_tool_calls"}, + } + for key, value in data.items(): + mapping = wire_map.get(key) + if mapping is not None and provider in mapping: + result[mapping[provider]] = value + return result + def to_yaml(self, context: SaveContext | None = None) -> str: """Convert the ModelOptions instance to a YAML string. Args: diff --git a/runtime/python/prompty/prompty/model/_Parser.py b/runtime/python/prompty/prompty/model/_Parser.py new file mode 100644 index 00000000..89df350a --- /dev/null +++ b/runtime/python/prompty/prompty/model/_Parser.py @@ -0,0 +1,27 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from typing import Any, Protocol, runtime_checkable + +from ._Message import Message +from ._Prompty import Prompty + + +@runtime_checkable +class Parser(Protocol): + """Parses rendered prompt text into an array of structured messages with role markers.""" + + def pre_render(self, template: str) -> Any | None: + """Pre-process a template before rendering, returning modified template and context""" + return None + + def parse(self, agent: Prompty, rendered: str, context: dict[str, Any] | None) -> list[Message]: + """Parse rendered text into a structured message array""" + ... + + async def parse_async(self, agent: Prompty, rendered: str, context: dict[str, Any] | None) -> list[Message]: + """Parse rendered text into a structured message array (async variant)""" + ... diff --git a/runtime/python/prompty/prompty/model/_ParserConfig.py b/runtime/python/prompty/prompty/model/_ParserConfig.py index 47510422..86bdd123 100644 --- a/runtime/python/prompty/prompty/model/_ParserConfig.py +++ b/runtime/python/prompty/prompty/model/_ParserConfig.py @@ -43,7 +43,11 @@ def load(data: Any, context: LoadContext | None = None) -> "ParserConfig": # handle alternate representations if isinstance(data, str): - data = {"kind": data} + instance = ParserConfig() + instance.kind = data + if context is not None: + instance = context.process_output(instance) + return instance if not isinstance(data, dict): raise ValueError(f"Invalid data for ParserConfig: {data}") diff --git a/runtime/python/prompty/prompty/model/_Processor.py b/runtime/python/prompty/prompty/model/_Processor.py new file mode 100644 index 00000000..a9f39a23 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_Processor.py @@ -0,0 +1,30 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from typing import Any, Protocol, runtime_checkable + +from ._Prompty import Prompty + + +@runtime_checkable +class Processor(Protocol): + """Extracts a clean, typed result from a raw LLM provider response.""" + + def process(self, agent: Prompty, response: Any) -> Any: + """Extract a clean result from a raw LLM response""" + ... + + async def process_async(self, agent: Prompty, response: Any) -> Any: + """Extract a clean result from a raw LLM response (async variant)""" + ... + + def process_stream(self, stream: Any) -> Any: + """Process a streaming response into a stream of StreamChunk items. Takes raw chunks from the executor and yields processed text, thinking, tool, or error chunks. Not all providers support streaming; the default implementation should signal lack of support.""" + return None + + async def process_stream_async(self, stream: Any) -> Any: + """Process a streaming response into a stream of StreamChunk items. Takes raw chunks from the executor and yields processed text, thinking, tool, or error chunks. Not all providers support streaming; the default implementation should signal lack of support. (async variant)""" + return None diff --git a/runtime/python/prompty/prompty/model/_Prompty.py b/runtime/python/prompty/prompty/model/_Prompty.py index 877db419..8c6b20aa 100644 --- a/runtime/python/prompty/prompty/model/_Prompty.py +++ b/runtime/python/prompty/prompty/model/_Prompty.py @@ -27,19 +27,19 @@ class or kind discriminator. A .prompty file always produces a Prompty instance. ---------- name : str Human-readable name of the prompt - displayName : Optional[str] + display_name : Optional[str] Display name for UI purposes description : Optional[str] Description of the prompt's purpose metadata : Optional[dict[str, Any]] Additional metadata including authors, tags, and other arbitrary properties - inputs : list[Property] + inputs : Optional[list[Property]] Input parameters that participate in template rendering - outputs : list[Property] + outputs : Optional[list[Property]] Expected output format and structure model : Model AI model configuration - tools : list[Tool] + tools : Optional[list[Tool]] Tools available for extended functionality template : Optional[Template] Template configuration for prompt rendering @@ -50,7 +50,7 @@ class or kind discriminator. A .prompty file always produces a Prompty instance. _shorthand_property: ClassVar[str | None] = None name: str = field(default="") - displayName: str | None = None + display_name: str | None = None description: str | None = None metadata: dict[str, Any] | None = None inputs: list[Property] = field(default_factory=list) @@ -83,7 +83,7 @@ def load(data: Any, context: LoadContext | None = None) -> "Prompty": if data is not None and "name" in data: instance.name = data["name"] if data is not None and "displayName" in data: - instance.displayName = data["displayName"] + instance.display_name = data["displayName"] if data is not None and "description" in data: instance.description = data["description"] if data is not None and "metadata" in data: @@ -158,7 +158,7 @@ def load_outputs(data: dict | list, context: LoadContext | None) -> list[Propert result.append({"name": k, **v}) else: # value is a scalar, use it as the primary property - result.append({"name": k, "": v}) + result.append({"name": k, "kind": v}) data = result return [Property.load(item, context) for item in data] @@ -167,28 +167,8 @@ def save_outputs(items: list[Property], context: SaveContext | None) -> dict[str if context is None: context = SaveContext() - if context.collection_format == "array": - return [item.save(context) for item in items] - - # Object format: use name as key - result: dict[str, Any] = {} - for item in items: - item_data = item.save(context) - name = item_data.pop("name", None) - if name: - # Check if we can use shorthand (only primary property set) - if context.use_shorthand and hasattr(item, "_shorthand_property"): - shorthand_prop = item._shorthand_property - if shorthand_prop and len(item_data) == 1 and shorthand_prop in item_data: - result[name] = item_data[shorthand_prop] - continue - result[name] = item_data - else: - # No name, fall back to array format for this item - if "_unnamed" not in result: - result["_unnamed"] = [] - result["_unnamed"].append(item_data) - return result + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] @staticmethod def load_tools(data: dict | list, context: LoadContext | None) -> list[Tool]: @@ -249,8 +229,8 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: if obj.name is not None: result["name"] = obj.name - if obj.displayName is not None: - result["displayName"] = obj.displayName + if obj.display_name is not None: + result["displayName"] = obj.display_name if obj.description is not None: result["description"] = obj.description if obj.metadata is not None: diff --git a/runtime/python/prompty/prompty/model/_Property.py b/runtime/python/prompty/prompty/model/_Property.py index 7e2f8270..198e1b2b 100644 --- a/runtime/python/prompty/prompty/model/_Property.py +++ b/runtime/python/prompty/prompty/model/_Property.py @@ -33,7 +33,7 @@ class Property: The default value of the property - this represents the default value if none is provided example : Optional[Any] Example value used for either initialization or tooling - enumValues : list[Any] + enum_values : Optional[list[Any]] Allowed enumeration values for the property """ @@ -45,7 +45,7 @@ class Property: required: bool | None = None default: Any | None = None example: Any | None = None - enumValues: list[Any] = field(default_factory=list) + enum_values: list[Any] = field(default_factory=list) @staticmethod def load(data: Any, context: LoadContext | None = None) -> "Property": @@ -63,13 +63,33 @@ def load(data: Any, context: LoadContext | None = None) -> "Property": # handle alternate representations if isinstance(data, bool): - data = {"kind": "boolean", "example": data} + instance = Property() + instance.kind = "boolean" + instance.example = data + if context is not None: + instance = context.process_output(instance) + return instance if isinstance(data, float): - data = {"kind": "float", "example": data} + instance = Property() + instance.kind = "float" + instance.example = data + if context is not None: + instance = context.process_output(instance) + return instance if isinstance(data, int): - data = {"kind": "integer", "example": data} + instance = Property() + instance.kind = "integer" + instance.example = data + if context is not None: + instance = context.process_output(instance) + return instance if isinstance(data, str): - data = {"kind": "string", "example": data} + instance = Property() + instance.kind = "string" + instance.example = data + if context is not None: + instance = context.process_output(instance) + return instance if not isinstance(data, dict): raise ValueError(f"Invalid data for Property: {data}") @@ -90,7 +110,7 @@ def load(data: Any, context: LoadContext | None = None) -> "Property": if data is not None and "example" in data: instance.example = data["example"] if data is not None and "enumValues" in data: - instance.enumValues = data["enumValues"] + instance.enum_values = data["enumValues"] if context is not None: instance = context.process_output(instance) return instance @@ -108,7 +128,6 @@ def load_kind(data: dict, context: LoadContext | None) -> "Property": else: # create new instance (stop recursion) return Property() - else: # create new instance return Property() @@ -139,8 +158,8 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["default"] = obj.default if obj.example is not None: result["example"] = obj.example - if obj.enumValues is not None: - result["enumValues"] = obj.enumValues + if obj.enum_values is not None: + result["enumValues"] = obj.enum_values if context is not None: result = context.process_dict(result) @@ -237,7 +256,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.items is not None: result["items"] = obj.items.save(context) - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -323,7 +341,7 @@ def load_properties(data: dict | list, context: LoadContext | None) -> list[Prop result.append({"name": k, **v}) else: # value is a scalar, use it as the primary property - result.append({"name": k, "": v}) + result.append({"name": k, "kind": v}) data = result return [Property.load(item, context) for item in data] @@ -332,28 +350,8 @@ def save_properties(items: list[Property], context: SaveContext | None) -> dict[ if context is None: context = SaveContext() - if context.collection_format == "array": - return [item.save(context) for item in items] - - # Object format: use name as key - result: dict[str, Any] = {} - for item in items: - item_data = item.save(context) - name = item_data.pop("name", None) - if name: - # Check if we can use shorthand (only primary property set) - if context.use_shorthand and hasattr(item, "_shorthand_property"): - shorthand_prop = item._shorthand_property - if shorthand_prop and len(item_data) == 1 and shorthand_prop in item_data: - result[name] = item_data[shorthand_prop] - continue - result[name] = item_data - else: - # No name, fall back to array format for this item - if "_unnamed" not in result: - result["_unnamed"] = [] - result["_unnamed"].append(item_data) - return result + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] def save(self, context: SaveContext | None = None) -> dict[str, Any]: """Save the ObjectProperty instance to a dictionary. @@ -374,7 +372,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.properties is not None: result["properties"] = ObjectProperty.save_properties(obj.properties, context) - return result def to_yaml(self, context: SaveContext | None = None) -> str: diff --git a/runtime/python/prompty/prompty/model/_Renderer.py b/runtime/python/prompty/prompty/model/_Renderer.py new file mode 100644 index 00000000..dddbfb3f --- /dev/null +++ b/runtime/python/prompty/prompty/model/_Renderer.py @@ -0,0 +1,22 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from typing import Any, Protocol, runtime_checkable + +from ._Prompty import Prompty + + +@runtime_checkable +class Renderer(Protocol): + """Renders a template string with input values to produce the final prompt text.""" + + def render(self, agent: Prompty, template: str, inputs: dict[str, Any]) -> str: + """Render the template string with input values""" + ... + + async def render_async(self, agent: Prompty, template: str, inputs: dict[str, Any]) -> str: + """Render the template string with input values (async variant)""" + ... diff --git a/runtime/python/prompty/prompty/model/_StatusEventPayload.py b/runtime/python/prompty/prompty/model/_StatusEventPayload.py new file mode 100644 index 00000000..6c75f91c --- /dev/null +++ b/runtime/python/prompty/prompty/model/_StatusEventPayload.py @@ -0,0 +1,97 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class StatusEventPayload: + """Payload for "status" events — informational messages about loop progress. + + Attributes + ---------- + message : str + Human-readable status message + """ + + _shorthand_property: ClassVar[str | None] = None + + message: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "StatusEventPayload": + """Load a StatusEventPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + StatusEventPayload: The loaded StatusEventPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for StatusEventPayload: {data}") + + # create new instance + instance = StatusEventPayload() + + if data is not None and "message" in data: + instance.message = data["message"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the StatusEventPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.message is not None: + result["message"] = obj.message + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the StatusEventPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the StatusEventPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_StreamChunk.py b/runtime/python/prompty/prompty/model/_StreamChunk.py new file mode 100644 index 00000000..db4abc19 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_StreamChunk.py @@ -0,0 +1,487 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from abc import ABC +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext +from ._ToolCall import ToolCall + + +@dataclass +class StreamChunk(ABC): + """A chunk of data from a streaming LLM response. Stream chunks are + discriminated on the `kind` field. + + Attributes + ---------- + kind : str + The kind of stream chunk + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "StreamChunk": + """Load a StreamChunk instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + StreamChunk: The loaded StreamChunk instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for StreamChunk: {data}") + + # load polymorphic StreamChunk instance + instance = StreamChunk.load_kind(data, context) + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_kind(data: dict, context: LoadContext | None) -> "StreamChunk": + # load polymorphic StreamChunk instance + if data is not None and "kind" in data: + discriminator_value = str(data["kind"]).lower() + if discriminator_value == "text": + return TextChunk.load(data, context) + elif discriminator_value == "thinking": + return ThinkingChunk.load(data, context) + elif discriminator_value == "tool": + return ToolChunk.load(data, context) + elif discriminator_value == "error": + return ErrorChunk.load(data, context) + + else: + raise ValueError(f"Unknown StreamChunk discriminator value: {discriminator_value}") + else: + raise ValueError("Missing StreamChunk discriminator property: 'kind'") + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the StreamChunk instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.kind is not None: + result["kind"] = obj.kind + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the StreamChunk instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the StreamChunk instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class TextChunk(StreamChunk): + """A text content chunk from the LLM response stream. + + Attributes + ---------- + kind : str + The kind identifier for text chunks + value : str + The text content of the chunk + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="text") + value: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "TextChunk": + """Load a TextChunk instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + TextChunk: The loaded TextChunk instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for TextChunk: {data}") + + # create new instance + instance = TextChunk() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "value" in data: + instance.value = data["value"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the TextChunk instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.value is not None: + result["value"] = obj.value + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the TextChunk instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the TextChunk instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class ThinkingChunk(StreamChunk): + """A thinking/reasoning content chunk from the LLM response stream. + + Attributes + ---------- + kind : str + The kind identifier for thinking chunks + value : str + The thinking content of the chunk + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="thinking") + value: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ThinkingChunk": + """Load a ThinkingChunk instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ThinkingChunk: The loaded ThinkingChunk instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ThinkingChunk: {data}") + + # create new instance + instance = ThinkingChunk() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "value" in data: + instance.value = data["value"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ThinkingChunk instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.value is not None: + result["value"] = obj.value + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ThinkingChunk instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ThinkingChunk instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class ToolChunk(StreamChunk): + """A tool call chunk from the LLM response stream. + + Attributes + ---------- + kind : str + The kind identifier for tool chunks + tool_call : ToolCall + The tool call data + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="tool") + tool_call: ToolCall = field(default_factory=ToolCall) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolChunk": + """Load a ToolChunk instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolChunk: The loaded ToolChunk instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolChunk: {data}") + + # create new instance + instance = ToolChunk() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "toolCall" in data: + instance.tool_call = ToolCall.load(data["toolCall"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolChunk instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.tool_call is not None: + result["toolCall"] = obj.tool_call.save(context) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolChunk instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolChunk instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + +@dataclass +class ErrorChunk(StreamChunk): + """An error chunk from the LLM response stream. + + Attributes + ---------- + kind : str + The kind identifier for error chunks + message : str + The error message + """ + + _shorthand_property: ClassVar[str | None] = None + + kind: str = field(default="error") + message: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ErrorChunk": + """Load a ErrorChunk instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ErrorChunk: The loaded ErrorChunk instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ErrorChunk: {data}") + + # create new instance + instance = ErrorChunk() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "message" in data: + instance.message = data["message"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ErrorChunk instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.message is not None: + result["message"] = obj.message + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ErrorChunk instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ErrorChunk instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ThinkingEventPayload.py b/runtime/python/prompty/prompty/model/_ThinkingEventPayload.py new file mode 100644 index 00000000..75717173 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ThinkingEventPayload.py @@ -0,0 +1,97 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ThinkingEventPayload: + """Payload for "thinking" events — reasoning/chain-of-thought tokens. + + Attributes + ---------- + token : str + The thinking/reasoning token text + """ + + _shorthand_property: ClassVar[str | None] = None + + token: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ThinkingEventPayload": + """Load a ThinkingEventPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ThinkingEventPayload: The loaded ThinkingEventPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ThinkingEventPayload: {data}") + + # create new instance + instance = ThinkingEventPayload() + + if data is not None and "token" in data: + instance.token = data["token"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ThinkingEventPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.token is not None: + result["token"] = obj.token + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ThinkingEventPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ThinkingEventPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ThreadMarker.py b/runtime/python/prompty/prompty/model/_ThreadMarker.py new file mode 100644 index 00000000..e348a499 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ThreadMarker.py @@ -0,0 +1,108 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ThreadMarker: + """Positional marker for conversation history insertion during template rendering. + + During `prepare()`, nonce strings in rendered text are replaced with + ThreadMarker objects. The pipeline then replaces them with actual + conversation messages from the inputs. + + Attributes + ---------- + name : str + The input property name (e.g. 'conversation') + kind : str + The rich kind ('thread', 'image', 'file', 'audio') + """ + + _shorthand_property: ClassVar[str | None] = None + + name: str = field(default="thread") + kind: str = field(default="thread") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ThreadMarker": + """Load a ThreadMarker instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ThreadMarker: The loaded ThreadMarker instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ThreadMarker: {data}") + + # create new instance + instance = ThreadMarker() + + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "kind" in data: + instance.kind = data["kind"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ThreadMarker instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.name is not None: + result["name"] = obj.name + if obj.kind is not None: + result["kind"] = obj.kind + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ThreadMarker instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ThreadMarker instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_TokenEventPayload.py b/runtime/python/prompty/prompty/model/_TokenEventPayload.py new file mode 100644 index 00000000..c0bd5efe --- /dev/null +++ b/runtime/python/prompty/prompty/model/_TokenEventPayload.py @@ -0,0 +1,97 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class TokenEventPayload: + """Payload for "token" events — a single text token streamed from the LLM. + + Attributes + ---------- + token : str + The streamed token text + """ + + _shorthand_property: ClassVar[str | None] = None + + token: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "TokenEventPayload": + """Load a TokenEventPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + TokenEventPayload: The loaded TokenEventPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for TokenEventPayload: {data}") + + # create new instance + instance = TokenEventPayload() + + if data is not None and "token" in data: + instance.token = data["token"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the TokenEventPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.token is not None: + result["token"] = obj.token + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the TokenEventPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the TokenEventPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_TokenUsage.py b/runtime/python/prompty/prompty/model/_TokenUsage.py new file mode 100644 index 00000000..f1fb2393 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_TokenUsage.py @@ -0,0 +1,134 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class TokenUsage: + """Tracks token consumption for a single LLM call. Provider-specific field + names (e.g., OpenAI's `prompt_tokens` vs Anthropic's `input_tokens`) + are mapped via `knownAs` augments in the wire directory. + + Attributes + ---------- + prompt_tokens : Optional[int] + Number of tokens in the prompt/input sent to the model + completion_tokens : Optional[int] + Number of tokens generated in the model's completion/output + total_tokens : Optional[int] + Total tokens consumed (prompt + completion). May be provided by the API or computed. + """ + + _shorthand_property: ClassVar[str | None] = None + + prompt_tokens: int | None = None + completion_tokens: int | None = None + total_tokens: int | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "TokenUsage": + """Load a TokenUsage instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + TokenUsage: The loaded TokenUsage instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for TokenUsage: {data}") + + # create new instance + instance = TokenUsage() + + if data is not None and "promptTokens" in data: + instance.prompt_tokens = data["promptTokens"] + if data is not None and "completionTokens" in data: + instance.completion_tokens = data["completionTokens"] + if data is not None and "totalTokens" in data: + instance.total_tokens = data["totalTokens"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the TokenUsage instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.prompt_tokens is not None: + result["promptTokens"] = obj.prompt_tokens + if obj.completion_tokens is not None: + result["completionTokens"] = obj.completion_tokens + if obj.total_tokens is not None: + result["totalTokens"] = obj.total_tokens + + if context is not None: + result = context.process_dict(result) + return result + + def to_wire(self, provider: str) -> dict[str, Any]: + """Convert to provider-specific wire format. + Args: + provider (str): The provider to convert to (e.g., "openai", "anthropic"). + Returns: + dict[str, Any]: The wire-format dictionary with provider-specific field names. + + """ + data = self.save() + result: dict[str, Any] = {} + wire_map: dict[str, dict[str, str]] = { + "promptTokens": {"openai": "prompt_tokens", "anthropic": "input_tokens"}, + "completionTokens": {"openai": "completion_tokens", "anthropic": "output_tokens"}, + "totalTokens": {"openai": "total_tokens"}, + } + for key, value in data.items(): + mapping = wire_map.get(key) + if mapping is not None and provider in mapping: + result[mapping[provider]] = value + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the TokenUsage instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the TokenUsage instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_Tool.py b/runtime/python/prompty/prompty/model/_Tool.py index 4dca47db..76d91071 100644 --- a/runtime/python/prompty/prompty/model/_Tool.py +++ b/runtime/python/prompty/prompty/model/_Tool.py @@ -27,7 +27,7 @@ class Tool(ABC): The kind identifier for the tool description : Optional[str] A short description of the tool for metadata purposes - bindings : list[Binding] + bindings : Optional[list[Binding]] Tool argument bindings to input properties """ @@ -130,7 +130,6 @@ def load_kind(data: dict, context: LoadContext | None) -> "Tool": else: # load default instance return CustomTool.load(data, context) - else: raise ValueError("Missing Tool discriminator property: 'kind'") @@ -248,7 +247,7 @@ def load_parameters(data: dict | list, context: LoadContext | None) -> list[Prop result.append({"name": k, **v}) else: # value is a scalar, use it as the primary property - result.append({"name": k, "": v}) + result.append({"name": k, "kind": v}) data = result return [Property.load(item, context) for item in data] @@ -257,28 +256,8 @@ def save_parameters(items: list[Property], context: SaveContext | None) -> dict[ if context is None: context = SaveContext() - if context.collection_format == "array": - return [item.save(context) for item in items] - - # Object format: use name as key - result: dict[str, Any] = {} - for item in items: - item_data = item.save(context) - name = item_data.pop("name", None) - if name: - # Check if we can use shorthand (only primary property set) - if context.use_shorthand and hasattr(item, "_shorthand_property"): - shorthand_prop = item._shorthand_property - if shorthand_prop and len(item_data) == 1 and shorthand_prop in item_data: - result[name] = item_data[shorthand_prop] - continue - result[name] = item_data - else: - # No name, fall back to array format for this item - if "_unnamed" not in result: - result["_unnamed"] = [] - result["_unnamed"].append(item_data) - return result + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] def save(self, context: SaveContext | None = None) -> dict[str, Any]: """Save the FunctionTool instance to a dictionary. @@ -301,7 +280,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["parameters"] = FunctionTool.save_parameters(obj.parameters, context) if obj.strict is not None: result["strict"] = obj.strict - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -405,7 +383,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["connection"] = obj.connection.save(context) if obj.options is not None: result["options"] = obj.options - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -444,13 +421,13 @@ class McpTool(Tool): The kind identifier for MCP tools connection : Connection The connection configuration for the MCP tool - serverName : str + server_name : str The server name of the MCP tool - serverDescription : Optional[str] + server_description : Optional[str] The description of the MCP tool - approvalMode : McpApprovalMode + approval_mode : McpApprovalMode The approval mode for the MCP tool - allowedTools : list[str] + allowed_tools : Optional[list[str]] List of allowed operations or resources for the MCP tool """ @@ -458,10 +435,10 @@ class McpTool(Tool): kind: str = field(default="mcp") connection: Connection = field(default_factory=Connection) - serverName: str = field(default="") - serverDescription: str | None = None - approvalMode: McpApprovalMode = field(default_factory=McpApprovalMode) - allowedTools: list[str] = field(default_factory=list) + server_name: str = field(default="") + server_description: str | None = None + approval_mode: McpApprovalMode = field(default_factory=McpApprovalMode) + allowed_tools: list[str] = field(default_factory=list) @staticmethod def load(data: Any, context: LoadContext | None = None) -> "McpTool": @@ -488,13 +465,13 @@ def load(data: Any, context: LoadContext | None = None) -> "McpTool": if data is not None and "connection" in data: instance.connection = Connection.load(data["connection"], context) if data is not None and "serverName" in data: - instance.serverName = data["serverName"] + instance.server_name = data["serverName"] if data is not None and "serverDescription" in data: - instance.serverDescription = data["serverDescription"] + instance.server_description = data["serverDescription"] if data is not None and "approvalMode" in data: - instance.approvalMode = McpApprovalMode.load(data["approvalMode"], context) + instance.approval_mode = McpApprovalMode.load(data["approvalMode"], context) if data is not None and "allowedTools" in data: - instance.allowedTools = data["allowedTools"] + instance.allowed_tools = data["allowedTools"] if context is not None: instance = context.process_output(instance) return instance @@ -518,15 +495,14 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["kind"] = obj.kind if obj.connection is not None: result["connection"] = obj.connection.save(context) - if obj.serverName is not None: - result["serverName"] = obj.serverName - if obj.serverDescription is not None: - result["serverDescription"] = obj.serverDescription - if obj.approvalMode is not None: - result["approvalMode"] = obj.approvalMode.save(context) - if obj.allowedTools is not None: - result["allowedTools"] = obj.allowedTools - + if obj.server_name is not None: + result["serverName"] = obj.server_name + if obj.server_description is not None: + result["serverDescription"] = obj.server_description + if obj.approval_mode is not None: + result["approvalMode"] = obj.approval_mode.save(context) + if obj.allowed_tools is not None: + result["allowedTools"] = obj.allowed_tools return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -626,7 +602,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["connection"] = obj.connection.save(context) if obj.specification is not None: result["specification"] = obj.specification - return result def to_yaml(self, context: SaveContext | None = None) -> str: @@ -729,7 +704,6 @@ def save(self, context: SaveContext | None = None) -> dict[str, Any]: result["path"] = obj.path if obj.mode is not None: result["mode"] = obj.mode - return result def to_yaml(self, context: SaveContext | None = None) -> str: diff --git a/runtime/python/prompty/prompty/model/_ToolCall.py b/runtime/python/prompty/prompty/model/_ToolCall.py new file mode 100644 index 00000000..336d2802 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ToolCall.py @@ -0,0 +1,112 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ToolCall: + """A tool call requested by the LLM. Contains the function name and serialized + arguments that should be dispatched to the appropriate tool handler. + + Attributes + ---------- + id : str + The unique identifier of the tool call + name : str + The name of the tool to call + arguments : str + The serialized JSON arguments for the tool call + """ + + _shorthand_property: ClassVar[str | None] = None + + id: str = field(default="") + name: str = field(default="") + arguments: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolCall": + """Load a ToolCall instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolCall: The loaded ToolCall instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolCall: {data}") + + # create new instance + instance = ToolCall() + + if data is not None and "id" in data: + instance.id = data["id"] + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "arguments" in data: + instance.arguments = data["arguments"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolCall instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.id is not None: + result["id"] = obj.id + if obj.name is not None: + result["name"] = obj.name + if obj.arguments is not None: + result["arguments"] = obj.arguments + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolCall instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolCall instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ToolCallStartPayload.py b/runtime/python/prompty/prompty/model/_ToolCallStartPayload.py new file mode 100644 index 00000000..a111c73d --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ToolCallStartPayload.py @@ -0,0 +1,104 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext + + +@dataclass +class ToolCallStartPayload: + """Payload for "tool_call_start" events — the LLM has requested a tool call. + + Attributes + ---------- + name : str + The name of the tool being called + arguments : str + The serialized JSON arguments for the tool call + """ + + _shorthand_property: ClassVar[str | None] = None + + name: str = field(default="") + arguments: str = field(default="") + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolCallStartPayload": + """Load a ToolCallStartPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolCallStartPayload: The loaded ToolCallStartPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolCallStartPayload: {data}") + + # create new instance + instance = ToolCallStartPayload() + + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "arguments" in data: + instance.arguments = data["arguments"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolCallStartPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.name is not None: + result["name"] = obj.name + if obj.arguments is not None: + result["arguments"] = obj.arguments + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolCallStartPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolCallStartPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ToolContext.py b/runtime/python/prompty/prompty/model/_ToolContext.py new file mode 100644 index 00000000..38c39187 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ToolContext.py @@ -0,0 +1,130 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext +from ._Message import Message + + +@dataclass +class ToolContext: + """Context passed to tool handlers during agent loop execution. Provides + access to the agent configuration, current conversation state, and + arbitrary metadata for tool implementations that need broader context. + + Attributes + ---------- + messages : list[Message] + The current conversation messages at the point of tool invocation + metadata : Optional[dict[str, Any]] + Optional metadata for tool-specific context (e.g., user session info) + """ + + _shorthand_property: ClassVar[str | None] = None + + messages: list[Message] = field(default_factory=list) + metadata: dict[str, Any] | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolContext": + """Load a ToolContext instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolContext: The loaded ToolContext instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolContext: {data}") + + # create new instance + instance = ToolContext() + + if data is not None and "messages" in data: + instance.messages = ToolContext.load_messages(data["messages"], context) + if data is not None and "metadata" in data: + instance.metadata = data["metadata"] + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_messages(data: dict | list, context: LoadContext | None) -> list[Message]: + if isinstance(data, dict): + # convert simple named messages to list of Message + result = [] + for k, v in data.items(): + if isinstance(v, dict): + # value is an object, spread its properties + result.append({"name": k, **v}) + else: + # value is a scalar, use it as the primary property + result.append({"name": k, "role": v}) + data = result + return [Message.load(item, context) for item in data] + + @staticmethod + def save_messages(items: list[Message], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]: + if context is None: + context = SaveContext() + + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolContext instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.messages is not None: + result["messages"] = ToolContext.save_messages(obj.messages, context) + if obj.metadata is not None: + result["metadata"] = obj.metadata + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolContext instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolContext instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ToolDispatchResult.py b/runtime/python/prompty/prompty/model/_ToolDispatchResult.py new file mode 100644 index 00000000..8088d660 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ToolDispatchResult.py @@ -0,0 +1,114 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext +from ._ToolResult import ToolResult + + +@dataclass +class ToolDispatchResult: + """The result of dispatching a single tool call. Pairs the tool call + identifier with the tool's name and result for correlation in the + agent loop's message assembly. + + Attributes + ---------- + tool_call_id : str + The tool call ID from the LLM response, used to correlate results + name : str + The name of the tool that was called + result : ToolResult + The result produced by the tool handler + """ + + _shorthand_property: ClassVar[str | None] = None + + tool_call_id: str = field(default="") + name: str = field(default="") + result: ToolResult = field(default_factory=ToolResult) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolDispatchResult": + """Load a ToolDispatchResult instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolDispatchResult: The loaded ToolDispatchResult instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolDispatchResult: {data}") + + # create new instance + instance = ToolDispatchResult() + + if data is not None and "toolCallId" in data: + instance.tool_call_id = data["toolCallId"] + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "result" in data: + instance.result = ToolResult.load(data["result"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolDispatchResult instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.tool_call_id is not None: + result["toolCallId"] = obj.tool_call_id + if obj.name is not None: + result["name"] = obj.name + if obj.result is not None: + result["result"] = obj.result.save(context) + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolDispatchResult instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolDispatchResult instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_ToolResult.py b/runtime/python/prompty/prompty/model/_ToolResult.py new file mode 100644 index 00000000..730786f9 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ToolResult.py @@ -0,0 +1,146 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar, Protocol, runtime_checkable + +from ._ContentPart import ContentPart, TextPart +from ._context import LoadContext, SaveContext + + +@dataclass +class ToolResult: + """The result of a tool execution. Contains a list of content parts, enabling + rich tool results (text, images, files, audio) rather than just strings. + + Implementations MUST support conversion from a plain string to a ToolResult + containing a single TextPart for backward compatibility. + + Attributes + ---------- + parts : list[ContentPart] + The content parts of the tool result + """ + + _shorthand_property: ClassVar[str | None] = None + + parts: list[ContentPart] = field(default_factory=list) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolResult": + """Load a ToolResult instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolResult: The loaded ToolResult instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolResult: {data}") + + # create new instance + instance = ToolResult() + + if data is not None and "parts" in data: + instance.parts = ToolResult.load_parts(data["parts"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_parts(data: dict | list, context: LoadContext | None) -> list[ContentPart]: + if isinstance(data, dict): + # convert simple named parts to list of ContentPart + result = [] + for k, v in data.items(): + if isinstance(v, dict): + # value is an object, spread its properties + result.append({"name": k, **v}) + else: + # value is a scalar, use it as the primary property + result.append({"name": k, "kind": v}) + data = result + return [ContentPart.load(item, context) for item in data] + + @staticmethod + def save_parts(items: list[ContentPart], context: SaveContext | None) -> dict[str, Any] | list[dict[str, Any]]: + if context is None: + context = SaveContext() + + # This type doesn't have a 'name' property, so always use array format + return [item.save(context) for item in items] + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolResult instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.parts is not None: + result["parts"] = ToolResult.save_parts(obj.parts, context) + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolResult instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolResult instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) + + @classmethod + def text(cls, value: str) -> "ToolResult": + """Create a ToolResult with preset field values.""" + return ToolResult(parts=[TextPart(value=value)]) + + +@runtime_checkable +class ToolResultHelpers(Protocol): + """Helper contract for ToolResult. + + Runtime implementations must provide these methods on every ToolResult + instance (either by attaching them to the generated class or by wrapping it). + The type checker can verify conformance by annotating against this Protocol + or by calling isinstance(instance, ToolResultHelpers) at runtime. + """ + + @property + def text(self) -> str: + """Concatenate all TextPart values joined by newline""" + ... diff --git a/runtime/python/prompty/prompty/model/_ToolResultPayload.py b/runtime/python/prompty/prompty/model/_ToolResultPayload.py new file mode 100644 index 00000000..28559e85 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_ToolResultPayload.py @@ -0,0 +1,105 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from ._context import LoadContext, SaveContext +from ._ToolResult import ToolResult + + +@dataclass +class ToolResultPayload: + """Payload for "tool_result" events — a tool has returned its result. + + Attributes + ---------- + name : str + The name of the tool that produced the result + result : ToolResult + The tool's result + """ + + _shorthand_property: ClassVar[str | None] = None + + name: str = field(default="") + result: ToolResult = field(default_factory=ToolResult) + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "ToolResultPayload": + """Load a ToolResultPayload instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolResultPayload: The loaded ToolResultPayload instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolResultPayload: {data}") + + # create new instance + instance = ToolResultPayload() + + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "result" in data: + instance.result = ToolResult.load(data["result"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the ToolResultPayload instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.name is not None: + result["name"] = obj.name + if obj.result is not None: + result["result"] = obj.result.save(context) + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the ToolResultPayload instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the ToolResultPayload instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/_TurnOptions.py b/runtime/python/prompty/prompty/model/_TurnOptions.py new file mode 100644 index 00000000..b6aca6f1 --- /dev/null +++ b/runtime/python/prompty/prompty/model/_TurnOptions.py @@ -0,0 +1,145 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass +from typing import Any, ClassVar + +from ._CompactionConfig import CompactionConfig +from ._context import LoadContext, SaveContext + + +@dataclass +class TurnOptions: + """Configuration for the agent loop's turn() function. Controls iteration + limits, retry policy, context management, and execution behavior. + + Runtimes accept these as either a TurnOptions object or individual + keyword/named parameters — the TypeSpec model defines the canonical + field set. + + Attributes + ---------- + max_iterations : Optional[int] + Maximum number of tool-call iterations before the loop terminates + max_llm_retries : Optional[int] + Maximum number of LLM call retries on transient failure within a single iteration + context_budget : Optional[int] + Character budget for the context window. When set, the loop trims older messages to stay within budget before each LLM call. System messages are never dropped. + parallel_tool_calls : Optional[bool] + Whether to execute multiple tool calls concurrently when the LLM returns several in one response + raw : Optional[bool] + When true, return the raw LLM response without processor post-processing + turn : Optional[int] + Turn number for trace labeling. Useful when the caller maintains an outer conversation loop. + compaction : Optional[CompactionConfig] + Context compaction configuration. Controls how messages are summarized when the context window exceeds the budget. + """ + + _shorthand_property: ClassVar[str | None] = None + + max_iterations: int | None = None + max_llm_retries: int | None = None + context_budget: int | None = None + parallel_tool_calls: bool | None = None + raw: bool | None = None + turn: int | None = None + compaction: CompactionConfig | None = None + + @staticmethod + def load(data: Any, context: LoadContext | None = None) -> "TurnOptions": + """Load a TurnOptions instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + TurnOptions: The loaded TurnOptions instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for TurnOptions: {data}") + + # create new instance + instance = TurnOptions() + + if data is not None and "maxIterations" in data: + instance.max_iterations = data["maxIterations"] + if data is not None and "maxLlmRetries" in data: + instance.max_llm_retries = data["maxLlmRetries"] + if data is not None and "contextBudget" in data: + instance.context_budget = data["contextBudget"] + if data is not None and "parallelToolCalls" in data: + instance.parallel_tool_calls = data["parallelToolCalls"] + if data is not None and "raw" in data: + instance.raw = data["raw"] + if data is not None and "turn" in data: + instance.turn = data["turn"] + if data is not None and "compaction" in data: + instance.compaction = CompactionConfig.load(data["compaction"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: SaveContext | None = None) -> dict[str, Any]: + """Save the TurnOptions instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.max_iterations is not None: + result["maxIterations"] = obj.max_iterations + if obj.max_llm_retries is not None: + result["maxLlmRetries"] = obj.max_llm_retries + if obj.context_budget is not None: + result["contextBudget"] = obj.context_budget + if obj.parallel_tool_calls is not None: + result["parallelToolCalls"] = obj.parallel_tool_calls + if obj.raw is not None: + result["raw"] = obj.raw + if obj.turn is not None: + result["turn"] = obj.turn + if obj.compaction is not None: + result["compaction"] = obj.compaction.save(context) + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: SaveContext | None = None) -> str: + """Convert the TurnOptions instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: SaveContext | None = None, indent: int = 2) -> str: + """Convert the TurnOptions instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/prompty/prompty/model/__init__.py b/runtime/python/prompty/prompty/model/__init__.py index f06bd034..a3c6c011 100644 --- a/runtime/python/prompty/prompty/model/__init__.py +++ b/runtime/python/prompty/prompty/model/__init__.py @@ -4,6 +4,9 @@ # ANY EDITS WILL BE LOST ########################################## from ._Binding import Binding +from ._CompactionCompletePayload import CompactionCompletePayload +from ._CompactionConfig import CompactionConfig +from ._CompactionFailedPayload import CompactionFailedPayload from ._Connection import ( AnonymousConnection, ApiKeyConnection, @@ -13,16 +16,69 @@ ReferenceConnection, RemoteConnection, ) +from ._ContentPart import ( + AudioPart, + ContentPart, + FilePart, + ImagePart, + TextPart, +) from ._context import LoadContext, SaveContext +from ._DoneEventPayload import DoneEventPayload +from ._ErrorEventPayload import ErrorEventPayload +from ._Executor import Executor from ._FormatConfig import FormatConfig +from ._GuardrailResult import GuardrailResult from ._McpApprovalMode import McpApprovalMode +from ._Message import ( + Message, + MessageHelpers, +) +from ._MessagesUpdatedPayload import MessagesUpdatedPayload from ._Model import Model +from ._ModelInfo import ModelInfo from ._ModelOptions import ModelOptions +from ._Parser import Parser from ._ParserConfig import ParserConfig +from ._Processor import Processor from ._Prompty import Prompty -from ._Property import ArrayProperty, ObjectProperty, Property +from ._Property import ( + ArrayProperty, + ObjectProperty, + Property, +) +from ._Renderer import Renderer +from ._StatusEventPayload import StatusEventPayload +from ._StreamChunk import ( + ErrorChunk, + StreamChunk, + TextChunk, + ThinkingChunk, + ToolChunk, +) from ._Template import Template -from ._Tool import CustomTool, FunctionTool, McpTool, OpenApiTool, PromptyTool, Tool +from ._ThinkingEventPayload import ThinkingEventPayload +from ._ThreadMarker import ThreadMarker +from ._TokenEventPayload import TokenEventPayload +from ._TokenUsage import TokenUsage +from ._Tool import ( + CustomTool, + FunctionTool, + McpTool, + OpenApiTool, + PromptyTool, + Tool, +) +from ._ToolCall import ToolCall +from ._ToolCallStartPayload import ToolCallStartPayload +from ._ToolContext import ToolContext +from ._ToolDispatchResult import ToolDispatchResult +from ._ToolResult import ( + ToolResult, + ToolResultHelpers, +) +from ._ToolResultPayload import ToolResultPayload +from ._TurnOptions import TurnOptions __all__ = [ "LoadContext", @@ -35,8 +91,8 @@ "RemoteConnection", "ApiKeyConnection", "AnonymousConnection", - "FoundryConnection", "OAuthConnection", + "FoundryConnection", "ModelOptions", "Model", "Binding", @@ -51,4 +107,41 @@ "ParserConfig", "Template", "Prompty", + "ContentPart", + "TextPart", + "ImagePart", + "FilePart", + "AudioPart", + "Message", + "MessageHelpers", + "ToolContext", + "ToolResult", + "ToolResultHelpers", + "ToolDispatchResult", + "ToolCall", + "GuardrailResult", + "ThreadMarker", + "TokenUsage", + "ModelInfo", + "CompactionConfig", + "TurnOptions", + "Renderer", + "Parser", + "Executor", + "Processor", + "TokenEventPayload", + "ThinkingEventPayload", + "ToolCallStartPayload", + "ToolResultPayload", + "StatusEventPayload", + "MessagesUpdatedPayload", + "DoneEventPayload", + "ErrorEventPayload", + "CompactionCompletePayload", + "CompactionFailedPayload", + "StreamChunk", + "TextChunk", + "ThinkingChunk", + "ToolChunk", + "ErrorChunk", ] diff --git a/runtime/python/prompty/prompty/providers/anthropic/executor.py b/runtime/python/prompty/prompty/providers/anthropic/executor.py index 74c587ef..4ed3db8a 100644 --- a/runtime/python/prompty/prompty/providers/anthropic/executor.py +++ b/runtime/python/prompty/prompty/providers/anthropic/executor.py @@ -130,20 +130,11 @@ def _build_options(agent: Prompty) -> dict[str, Any]: if opts is None: return {} - result: dict[str, Any] = {} - - if opts.temperature is not None: - result["temperature"] = opts.temperature - if opts.topP is not None: - result["top_p"] = opts.topP - if opts.topK is not None: - result["top_k"] = opts.topK - if opts.stopSequences: - result["stop_sequences"] = opts.stopSequences + result = opts.to_wire("anthropic") # Pass through additionalProperties - if opts.additionalProperties: - for k, v in opts.additionalProperties.items(): + if opts.additional_properties: + for k, v in opts.additional_properties.items(): if k not in result and k != "max_tokens": result[k] = v @@ -170,8 +161,8 @@ def _property_to_json_schema(prop: Any) -> dict[str, Any]: if hasattr(prop, "description") and prop.description: schema["description"] = prop.description - if hasattr(prop, "enumValues") and prop.enumValues: - schema["enum"] = prop.enumValues + if hasattr(prop, "enum_values") and prop.enum_values: + schema["enum"] = prop.enum_values if getattr(prop, "kind", None) == "array": items = getattr(prop, "items", None) @@ -289,15 +280,14 @@ def _build_chat_args(agent: Prompty, messages: list[Message]) -> dict[str, Any]: else: conversation.append(_message_to_wire(msg)) - max_tokens = DEFAULT_MAX_TOKENS - if agent.model.options and agent.model.options.maxOutputTokens is not None: - max_tokens = agent.model.options.maxOutputTokens + opts = _build_options(agent) + if "max_tokens" not in opts: + opts["max_tokens"] = DEFAULT_MAX_TOKENS args: dict[str, Any] = { "model": model, "messages": conversation, - "max_tokens": max_tokens, - **_build_options(agent), + **opts, } if system_parts: @@ -333,7 +323,7 @@ class AnthropicExecutor: @trace def execute(self, agent: Prompty, data: Any) -> Any: client = self._resolve_client(agent) - api_type = agent.model.apiType or "chat" + api_type = agent.model.api_type or "chat" if api_type == "chat": return self._execute_chat(client, agent, data) @@ -345,7 +335,7 @@ def execute(self, agent: Prompty, data: Any) -> Any: @trace async def execute_async(self, agent: Prompty, data: Any) -> Any: client = self._resolve_client_async(agent) - api_type = agent.model.apiType or "chat" + api_type = agent.model.api_type or "chat" if api_type == "chat": return await self._execute_chat_async(client, agent, data) @@ -413,8 +403,8 @@ def _client_kwargs(self, agent: Prompty) -> dict[str, Any]: conn = agent.model.connection if isinstance(conn, ApiKeyConnection): - if conn.apiKey: - kwargs["api_key"] = conn.apiKey + if conn.api_key: + kwargs["api_key"] = conn.api_key if conn.endpoint: kwargs["base_url"] = conn.endpoint elif conn: diff --git a/runtime/python/prompty/prompty/providers/foundry/executor.py b/runtime/python/prompty/prompty/providers/foundry/executor.py index 24886754..e0485b8e 100644 --- a/runtime/python/prompty/prompty/providers/foundry/executor.py +++ b/runtime/python/prompty/prompty/providers/foundry/executor.py @@ -50,7 +50,7 @@ class FoundryExecutor(_BaseExecutor): @trace def execute(self, agent: Prompty, data: Any) -> Any: client = self._resolve_client(agent) - api_type = agent.model.apiType or "chat" + api_type = agent.model.api_type or "chat" if api_type == "chat": return self._execute_chat(client, agent, data) @@ -66,7 +66,7 @@ def execute(self, agent: Prompty, data: Any) -> Any: @trace async def execute_async(self, agent: Prompty, data: Any) -> Any: client = self._resolve_client_async(agent) - api_type = agent.model.apiType or "chat" + api_type = agent.model.api_type or "chat" if api_type == "chat": return await self._execute_chat_async(client, agent, data) @@ -117,7 +117,7 @@ def _build_client_from_key(self, conn: ApiKeyConnection) -> Any: """Build a sync AzureOpenAI client from an ApiKeyConnection.""" from openai import AzureOpenAI - if not conn.apiKey: + if not conn.api_key: raise ValueError( "Foundry connection has kind 'key' but no apiKey. " "Either provide an apiKey (e.g., apiKey: ${env:AZURE_OPENAI_API_KEY}), " @@ -128,7 +128,7 @@ def _build_client_from_key(self, conn: ApiKeyConnection) -> Any: "Then call: prompty.register_connection('my-foundry', client=AzureOpenAI(...))" ) - kwargs: dict[str, Any] = {"api_key": conn.apiKey, "api_version": "2024-12-01-preview"} + kwargs: dict[str, Any] = {"api_key": conn.api_key, "api_version": "2024-12-01-preview"} if conn.endpoint: kwargs["azure_endpoint"] = conn.endpoint @@ -148,7 +148,7 @@ def _build_async_client_from_key(self, conn: ApiKeyConnection) -> Any: """Build an async AsyncAzureOpenAI client from an ApiKeyConnection.""" from openai import AsyncAzureOpenAI - if not conn.apiKey: + if not conn.api_key: raise ValueError( "Foundry connection has kind 'key' but no apiKey. " "Either provide an apiKey (e.g., apiKey: ${env:AZURE_OPENAI_API_KEY}), " @@ -159,7 +159,7 @@ def _build_async_client_from_key(self, conn: ApiKeyConnection) -> Any: "Then call: prompty.register_connection('my-foundry', client=AsyncAzureOpenAI(...))" ) - kwargs: dict[str, Any] = {"api_key": conn.apiKey, "api_version": "2024-12-01-preview"} + kwargs: dict[str, Any] = {"api_key": conn.api_key, "api_version": "2024-12-01-preview"} if conn.endpoint: kwargs["azure_endpoint"] = conn.endpoint diff --git a/runtime/python/prompty/prompty/providers/openai/executor.py b/runtime/python/prompty/prompty/providers/openai/executor.py index bff668c7..ded8fe09 100644 --- a/runtime/python/prompty/prompty/providers/openai/executor.py +++ b/runtime/python/prompty/prompty/providers/openai/executor.py @@ -204,8 +204,8 @@ def _property_to_json_schema(prop) -> dict[str, Any]: if prop.description: schema["description"] = prop.description - if prop.enumValues: - schema["enum"] = prop.enumValues + if prop.enum_values: + schema["enum"] = prop.enum_values # Array items — default to string if unspecified if prop.kind == "array": @@ -270,30 +270,15 @@ def _output_schema_to_wire(agent: Prompty) -> dict[str, Any] | None: def _build_options(agent: Prompty) -> dict[str, Any]: """Extract model options into kwargs for the chat completions API call.""" - opts: dict[str, Any] = {} if agent.model.options is None: - return opts + return {} mo = agent.model.options - - if mo.temperature is not None: - opts["temperature"] = mo.temperature - if mo.maxOutputTokens is not None: - opts["max_completion_tokens"] = mo.maxOutputTokens - if mo.topP is not None: - opts["top_p"] = mo.topP - if mo.frequencyPenalty is not None: - opts["frequency_penalty"] = mo.frequencyPenalty - if mo.presencePenalty is not None: - opts["presence_penalty"] = mo.presencePenalty - if mo.seed is not None: - opts["seed"] = mo.seed - if mo.stopSequences: - opts["stop"] = mo.stopSequences + opts = mo.to_wire("openai") # Pass through additional properties - if mo.additionalProperties: - for k, v in mo.additionalProperties.items(): + if mo.additional_properties: + for k, v in mo.additional_properties.items(): if k not in opts: opts[k] = v @@ -307,22 +292,15 @@ def _build_options(agent: Prompty) -> dict[str, Any]: def _build_responses_options(agent: Prompty) -> dict[str, Any]: """Extract model options for the Responses API (different param names).""" - opts: dict[str, Any] = {} if agent.model.options is None: - return opts + return {} mo = agent.model.options - - if mo.temperature is not None: - opts["temperature"] = mo.temperature - if mo.maxOutputTokens is not None: - opts["max_output_tokens"] = mo.maxOutputTokens - if mo.topP is not None: - opts["top_p"] = mo.topP + opts = mo.to_wire("responses") # Pass through additional properties - if mo.additionalProperties: - for k, v in mo.additionalProperties.items(): + if mo.additional_properties: + for k, v in mo.additional_properties.items(): if k not in opts: opts[k] = v @@ -737,7 +715,7 @@ class OpenAIExecutor(_BaseExecutor): @trace def execute(self, agent: Prompty, data: Any) -> Any: client = self._resolve_client(agent) - api_type = agent.model.apiType or "chat" + api_type = agent.model.api_type or "chat" if api_type == "chat": return self._execute_chat(client, agent, data) @@ -753,7 +731,7 @@ def execute(self, agent: Prompty, data: Any) -> Any: @trace async def execute_async(self, agent: Prompty, data: Any) -> Any: client = self._resolve_client_async(agent) - api_type = agent.model.apiType or "chat" + api_type = agent.model.api_type or "chat" if api_type == "chat": return await self._execute_chat_async(client, agent, data) @@ -815,8 +793,8 @@ def _client_kwargs(self, agent: Prompty) -> dict[str, Any]: kwargs: dict[str, Any] = {} conn = agent.model.connection if conn and isinstance(conn, ApiKeyConnection): - if conn.apiKey: - kwargs["api_key"] = conn.apiKey + if conn.api_key: + kwargs["api_key"] = conn.api_key if conn.endpoint: kwargs["base_url"] = conn.endpoint elif conn: diff --git a/runtime/python/prompty/tests/model/test_api_key_connection.py b/runtime/python/prompty/tests/model/test_api_key_connection.py index 73cf55ea..79616486 100644 --- a/runtime/python/prompty/tests/model/test_api_key_connection.py +++ b/runtime/python/prompty/tests/model/test_api_key_connection.py @@ -18,7 +18,7 @@ def test_load_json_apikeyconnection(): assert instance is not None assert instance.kind == "key" assert instance.endpoint == "https://{your-custom-endpoint}.openai.azure.com/" - assert instance.apiKey == "your-api-key" + assert instance.api_key == "your-api-key" def test_load_yaml_apikeyconnection(): @@ -33,7 +33,7 @@ def test_load_yaml_apikeyconnection(): assert instance is not None assert instance.kind == "key" assert instance.endpoint == "https://{your-custom-endpoint}.openai.azure.com/" - assert instance.apiKey == "your-api-key" + assert instance.api_key == "your-api-key" def test_roundtrip_json_apikeyconnection(): @@ -52,7 +52,7 @@ def test_roundtrip_json_apikeyconnection(): assert reloaded is not None assert reloaded.kind == "key" assert reloaded.endpoint == "https://{your-custom-endpoint}.openai.azure.com/" - assert reloaded.apiKey == "your-api-key" + assert reloaded.api_key == "your-api-key" def test_to_json_apikeyconnection(): diff --git a/runtime/python/prompty/tests/model/test_audio_part.py b/runtime/python/prompty/tests/model/test_audio_part.py new file mode 100644 index 00000000..aa6fb24a --- /dev/null +++ b/runtime/python/prompty/tests/model/test_audio_part.py @@ -0,0 +1,81 @@ +import json + +import yaml + +from prompty.model import AudioPart + + +def test_load_json_audiopart(): + json_data = r""" + { + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" + } + """ + data = json.loads(json_data, strict=False) + instance = AudioPart.load(data) + assert instance is not None + assert instance.source == "https://example.com/audio.wav" + assert instance.media_type == "audio/wav" + + +def test_load_yaml_audiopart(): + yaml_data = r""" + source: "https://example.com/audio.wav" + mediaType: audio/wav + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = AudioPart.load(data) + assert instance is not None + assert instance.source == "https://example.com/audio.wav" + assert instance.media_type == "audio/wav" + + +def test_roundtrip_json_audiopart(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" + } + """ + original_data = json.loads(json_data, strict=False) + instance = AudioPart.load(original_data) + saved_data = instance.save() + reloaded = AudioPart.load(saved_data) + assert reloaded is not None + assert reloaded.source == "https://example.com/audio.wav" + assert reloaded.media_type == "audio/wav" + + +def test_to_json_audiopart(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" + } + """ + data = json.loads(json_data, strict=False) + instance = AudioPart.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_audiopart(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" + } + """ + data = json.loads(json_data, strict=False) + instance = AudioPart.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_compaction_complete_payload.py b/runtime/python/prompty/tests/model/test_compaction_complete_payload.py new file mode 100644 index 00000000..e235eb85 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_compaction_complete_payload.py @@ -0,0 +1,81 @@ +import json + +import yaml + +from prompty.model import CompactionCompletePayload + + +def test_load_json_compactioncompletepayload(): + json_data = r""" + { + "removed": 5, + "remaining": 3 + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionCompletePayload.load(data) + assert instance is not None + assert instance.removed == 5 + assert instance.remaining == 3 + + +def test_load_yaml_compactioncompletepayload(): + yaml_data = r""" + removed: 5 + remaining: 3 + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = CompactionCompletePayload.load(data) + assert instance is not None + assert instance.removed == 5 + assert instance.remaining == 3 + + +def test_roundtrip_json_compactioncompletepayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "removed": 5, + "remaining": 3 + } + """ + original_data = json.loads(json_data, strict=False) + instance = CompactionCompletePayload.load(original_data) + saved_data = instance.save() + reloaded = CompactionCompletePayload.load(saved_data) + assert reloaded is not None + assert reloaded.removed == 5 + assert reloaded.remaining == 3 + + +def test_to_json_compactioncompletepayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "removed": 5, + "remaining": 3 + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionCompletePayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_compactioncompletepayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "removed": 5, + "remaining": 3 + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionCompletePayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_compaction_config.py b/runtime/python/prompty/tests/model/test_compaction_config.py new file mode 100644 index 00000000..ce637de9 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_compaction_config.py @@ -0,0 +1,95 @@ +import json + +import yaml + +from prompty.model import CompactionConfig + + +def test_load_json_compactionconfig(): + json_data = r""" + { + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionConfig.load(data) + assert instance is not None + assert instance.strategy == "summarize" + assert instance.budget == 50000 + + +def test_load_yaml_compactionconfig(): + yaml_data = r""" + strategy: summarize + budget: 50000 + options: + preserveSystemMessages: true + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = CompactionConfig.load(data) + assert instance is not None + assert instance.strategy == "summarize" + assert instance.budget == 50000 + + +def test_roundtrip_json_compactionconfig(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = CompactionConfig.load(original_data) + saved_data = instance.save() + reloaded = CompactionConfig.load(saved_data) + assert reloaded is not None + assert reloaded.strategy == "summarize" + assert reloaded.budget == 50000 + + +def test_to_json_compactionconfig(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionConfig.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_compactionconfig(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionConfig.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_compaction_failed_payload.py b/runtime/python/prompty/tests/model/test_compaction_failed_payload.py new file mode 100644 index 00000000..d7ef0d06 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_compaction_failed_payload.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import CompactionFailedPayload + + +def test_load_json_compactionfailedpayload(): + json_data = r""" + { + "message": "Summarization prompt exceeded context window" + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionFailedPayload.load(data) + assert instance is not None + assert instance.message == "Summarization prompt exceeded context window" + + +def test_load_yaml_compactionfailedpayload(): + yaml_data = r""" + message: Summarization prompt exceeded context window + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = CompactionFailedPayload.load(data) + assert instance is not None + assert instance.message == "Summarization prompt exceeded context window" + + +def test_roundtrip_json_compactionfailedpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "message": "Summarization prompt exceeded context window" + } + """ + original_data = json.loads(json_data, strict=False) + instance = CompactionFailedPayload.load(original_data) + saved_data = instance.save() + reloaded = CompactionFailedPayload.load(saved_data) + assert reloaded is not None + assert reloaded.message == "Summarization prompt exceeded context window" + + +def test_to_json_compactionfailedpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "message": "Summarization prompt exceeded context window" + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionFailedPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_compactionfailedpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "message": "Summarization prompt exceeded context window" + } + """ + data = json.loads(json_data, strict=False) + instance = CompactionFailedPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_connection.py b/runtime/python/prompty/tests/model/test_connection.py index ee058346..872a0998 100644 --- a/runtime/python/prompty/tests/model/test_connection.py +++ b/runtime/python/prompty/tests/model/test_connection.py @@ -17,8 +17,8 @@ def test_load_json_connection(): instance = Connection.load(data) assert instance is not None assert instance.kind == "reference" - assert instance.authenticationMode == "system" - assert instance.usageDescription == "This will allow the agent to respond to an email on your behalf" + assert instance.authentication_mode == "system" + assert instance.usage_description == "This will allow the agent to respond to an email on your behalf" def test_load_yaml_connection(): @@ -32,8 +32,8 @@ def test_load_yaml_connection(): instance = Connection.load(data) assert instance is not None assert instance.kind == "reference" - assert instance.authenticationMode == "system" - assert instance.usageDescription == "This will allow the agent to respond to an email on your behalf" + assert instance.authentication_mode == "system" + assert instance.usage_description == "This will allow the agent to respond to an email on your behalf" def test_roundtrip_json_connection(): @@ -51,8 +51,8 @@ def test_roundtrip_json_connection(): reloaded = Connection.load(saved_data) assert reloaded is not None assert reloaded.kind == "reference" - assert reloaded.authenticationMode == "system" - assert reloaded.usageDescription == "This will allow the agent to respond to an email on your behalf" + assert reloaded.authentication_mode == "system" + assert reloaded.usage_description == "This will allow the agent to respond to an email on your behalf" def test_to_json_connection(): diff --git a/runtime/python/prompty/tests/model/test_content_part.py b/runtime/python/prompty/tests/model/test_content_part.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_content_part.py @@ -0,0 +1 @@ + diff --git a/runtime/python/prompty/tests/model/test_done_event_payload.py b/runtime/python/prompty/tests/model/test_done_event_payload.py new file mode 100644 index 00000000..fbc99566 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_done_event_payload.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import DoneEventPayload + + +def test_load_json_doneeventpayload(): + json_data = r""" + { + "response": "The weather in Paris is 72°F and sunny." + } + """ + data = json.loads(json_data, strict=False) + instance = DoneEventPayload.load(data) + assert instance is not None + assert instance.response == "The weather in Paris is 72°F and sunny." + + +def test_load_yaml_doneeventpayload(): + yaml_data = r""" + response: The weather in Paris is 72°F and sunny. + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = DoneEventPayload.load(data) + assert instance is not None + assert instance.response == "The weather in Paris is 72°F and sunny." + + +def test_roundtrip_json_doneeventpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "response": "The weather in Paris is 72°F and sunny." + } + """ + original_data = json.loads(json_data, strict=False) + instance = DoneEventPayload.load(original_data) + saved_data = instance.save() + reloaded = DoneEventPayload.load(saved_data) + assert reloaded is not None + assert reloaded.response == "The weather in Paris is 72°F and sunny." + + +def test_to_json_doneeventpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "response": "The weather in Paris is 72°F and sunny." + } + """ + data = json.loads(json_data, strict=False) + instance = DoneEventPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_doneeventpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "response": "The weather in Paris is 72°F and sunny." + } + """ + data = json.loads(json_data, strict=False) + instance = DoneEventPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_error_chunk.py b/runtime/python/prompty/tests/model/test_error_chunk.py new file mode 100644 index 00000000..2116a0b5 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_error_chunk.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import ErrorChunk + + +def test_load_json_errorchunk(): + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + data = json.loads(json_data, strict=False) + instance = ErrorChunk.load(data) + assert instance is not None + assert instance.message == "Rate limit exceeded" + + +def test_load_yaml_errorchunk(): + yaml_data = r""" + message: Rate limit exceeded + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ErrorChunk.load(data) + assert instance is not None + assert instance.message == "Rate limit exceeded" + + +def test_roundtrip_json_errorchunk(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + original_data = json.loads(json_data, strict=False) + instance = ErrorChunk.load(original_data) + saved_data = instance.save() + reloaded = ErrorChunk.load(saved_data) + assert reloaded is not None + assert reloaded.message == "Rate limit exceeded" + + +def test_to_json_errorchunk(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + data = json.loads(json_data, strict=False) + instance = ErrorChunk.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_errorchunk(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + data = json.loads(json_data, strict=False) + instance = ErrorChunk.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_error_event_payload.py b/runtime/python/prompty/tests/model/test_error_event_payload.py new file mode 100644 index 00000000..a68637b2 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_error_event_payload.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import ErrorEventPayload + + +def test_load_json_erroreventpayload(): + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + data = json.loads(json_data, strict=False) + instance = ErrorEventPayload.load(data) + assert instance is not None + assert instance.message == "Rate limit exceeded" + + +def test_load_yaml_erroreventpayload(): + yaml_data = r""" + message: Rate limit exceeded + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ErrorEventPayload.load(data) + assert instance is not None + assert instance.message == "Rate limit exceeded" + + +def test_roundtrip_json_erroreventpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + original_data = json.loads(json_data, strict=False) + instance = ErrorEventPayload.load(original_data) + saved_data = instance.save() + reloaded = ErrorEventPayload.load(saved_data) + assert reloaded is not None + assert reloaded.message == "Rate limit exceeded" + + +def test_to_json_erroreventpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + data = json.loads(json_data, strict=False) + instance = ErrorEventPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_erroreventpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "message": "Rate limit exceeded" + } + """ + data = json.loads(json_data, strict=False) + instance = ErrorEventPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_file_part.py b/runtime/python/prompty/tests/model/test_file_part.py new file mode 100644 index 00000000..f47505b4 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_file_part.py @@ -0,0 +1,81 @@ +import json + +import yaml + +from prompty.model import FilePart + + +def test_load_json_filepart(): + json_data = r""" + { + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" + } + """ + data = json.loads(json_data, strict=False) + instance = FilePart.load(data) + assert instance is not None + assert instance.source == "https://example.com/document.pdf" + assert instance.media_type == "application/pdf" + + +def test_load_yaml_filepart(): + yaml_data = r""" + source: "https://example.com/document.pdf" + mediaType: application/pdf + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = FilePart.load(data) + assert instance is not None + assert instance.source == "https://example.com/document.pdf" + assert instance.media_type == "application/pdf" + + +def test_roundtrip_json_filepart(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" + } + """ + original_data = json.loads(json_data, strict=False) + instance = FilePart.load(original_data) + saved_data = instance.save() + reloaded = FilePart.load(saved_data) + assert reloaded is not None + assert reloaded.source == "https://example.com/document.pdf" + assert reloaded.media_type == "application/pdf" + + +def test_to_json_filepart(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" + } + """ + data = json.loads(json_data, strict=False) + instance = FilePart.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_filepart(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" + } + """ + data = json.loads(json_data, strict=False) + instance = FilePart.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_format_config.py b/runtime/python/prompty/tests/model/test_format_config.py index 436f17db..f02d7108 100644 --- a/runtime/python/prompty/tests/model/test_format_config.py +++ b/runtime/python/prompty/tests/model/test_format_config.py @@ -19,7 +19,6 @@ def test_load_json_formatconfig(): instance = FormatConfig.load(data) assert instance is not None assert instance.kind == "mustache" - assert instance.strict diff --git a/runtime/python/prompty/tests/model/test_foundry_connection.py b/runtime/python/prompty/tests/model/test_foundry_connection.py index d473bdca..8cb16477 100644 --- a/runtime/python/prompty/tests/model/test_foundry_connection.py +++ b/runtime/python/prompty/tests/model/test_foundry_connection.py @@ -1,97 +1,97 @@ -import json - -import yaml - -from prompty.model import FoundryConnection - - -def test_load_json_foundryconnection(): - json_data = r""" - { - "kind": "foundry", - "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", - "name": "my-openai-connection", - "connectionType": "model" - } - """ - data = json.loads(json_data, strict=False) - instance = FoundryConnection.load(data) - assert instance is not None - assert instance.kind == "foundry" - assert instance.endpoint == "https://myresource.services.ai.azure.com/api/projects/myproject" - assert instance.name == "my-openai-connection" - assert instance.connectionType == "model" - - -def test_load_yaml_foundryconnection(): - yaml_data = r""" - kind: foundry - endpoint: "https://myresource.services.ai.azure.com/api/projects/myproject" - name: my-openai-connection - connectionType: model - - """ - data = yaml.load(yaml_data, Loader=yaml.FullLoader) - instance = FoundryConnection.load(data) - assert instance is not None - assert instance.kind == "foundry" - assert instance.endpoint == "https://myresource.services.ai.azure.com/api/projects/myproject" - assert instance.name == "my-openai-connection" - assert instance.connectionType == "model" - - -def test_roundtrip_json_foundryconnection(): - """Test that load -> save -> load produces equivalent data.""" - json_data = r""" - { - "kind": "foundry", - "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", - "name": "my-openai-connection", - "connectionType": "model" - } - """ - original_data = json.loads(json_data, strict=False) - instance = FoundryConnection.load(original_data) - saved_data = instance.save() - reloaded = FoundryConnection.load(saved_data) - assert reloaded is not None - assert reloaded.kind == "foundry" - assert reloaded.endpoint == "https://myresource.services.ai.azure.com/api/projects/myproject" - assert reloaded.name == "my-openai-connection" - assert reloaded.connectionType == "model" - - -def test_to_json_foundryconnection(): - """Test that to_json produces valid JSON.""" - json_data = r""" - { - "kind": "foundry", - "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", - "name": "my-openai-connection", - "connectionType": "model" - } - """ - data = json.loads(json_data, strict=False) - instance = FoundryConnection.load(data) - json_output = instance.to_json() - assert json_output is not None - parsed = json.loads(json_output) - assert isinstance(parsed, dict) - - -def test_to_yaml_foundryconnection(): - """Test that to_yaml produces valid YAML.""" - json_data = r""" - { - "kind": "foundry", - "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", - "name": "my-openai-connection", - "connectionType": "model" - } - """ - data = json.loads(json_data, strict=False) - instance = FoundryConnection.load(data) - yaml_output = instance.to_yaml() - assert yaml_output is not None - parsed = yaml.safe_load(yaml_output) - assert isinstance(parsed, dict) +import json + +import yaml + +from prompty.model import FoundryConnection + + +def test_load_json_foundryconnection(): + json_data = r""" + { + "kind": "foundry", + "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", + "name": "my-openai-connection", + "connectionType": "model" + } + """ + data = json.loads(json_data, strict=False) + instance = FoundryConnection.load(data) + assert instance is not None + assert instance.kind == "foundry" + assert instance.endpoint == "https://myresource.services.ai.azure.com/api/projects/myproject" + assert instance.name == "my-openai-connection" + assert instance.connection_type == "model" + + +def test_load_yaml_foundryconnection(): + yaml_data = r""" + kind: foundry + endpoint: "https://myresource.services.ai.azure.com/api/projects/myproject" + name: my-openai-connection + connectionType: model + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = FoundryConnection.load(data) + assert instance is not None + assert instance.kind == "foundry" + assert instance.endpoint == "https://myresource.services.ai.azure.com/api/projects/myproject" + assert instance.name == "my-openai-connection" + assert instance.connection_type == "model" + + +def test_roundtrip_json_foundryconnection(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "kind": "foundry", + "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", + "name": "my-openai-connection", + "connectionType": "model" + } + """ + original_data = json.loads(json_data, strict=False) + instance = FoundryConnection.load(original_data) + saved_data = instance.save() + reloaded = FoundryConnection.load(saved_data) + assert reloaded is not None + assert reloaded.kind == "foundry" + assert reloaded.endpoint == "https://myresource.services.ai.azure.com/api/projects/myproject" + assert reloaded.name == "my-openai-connection" + assert reloaded.connection_type == "model" + + +def test_to_json_foundryconnection(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "kind": "foundry", + "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", + "name": "my-openai-connection", + "connectionType": "model" + } + """ + data = json.loads(json_data, strict=False) + instance = FoundryConnection.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_foundryconnection(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "kind": "foundry", + "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", + "name": "my-openai-connection", + "connectionType": "model" + } + """ + data = json.loads(json_data, strict=False) + instance = FoundryConnection.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_function_tool.py b/runtime/python/prompty/tests/model/test_function_tool.py index ae4aef36..fbecb18f 100644 --- a/runtime/python/prompty/tests/model/test_function_tool.py +++ b/runtime/python/prompty/tests/model/test_function_tool.py @@ -30,7 +30,6 @@ def test_load_json_functiontool(): instance = FunctionTool.load(data) assert instance is not None assert instance.kind == "function" - assert instance.strict @@ -176,7 +175,6 @@ def test_load_json_functiontool_1(): instance = FunctionTool.load(data) assert instance is not None assert instance.kind == "function" - assert instance.strict diff --git a/runtime/python/prompty/tests/model/test_guardrail_result.py b/runtime/python/prompty/tests/model/test_guardrail_result.py new file mode 100644 index 00000000..c0ae828d --- /dev/null +++ b/runtime/python/prompty/tests/model/test_guardrail_result.py @@ -0,0 +1,105 @@ +import json + +import yaml + +from prompty.model import GuardrailResult + + +def test_load_json_guardrailresult(): + json_data = r""" + { + "allowed": true, + "reason": "Content is safe" + } + """ + data = json.loads(json_data, strict=False) + instance = GuardrailResult.load(data) + assert instance is not None + assert instance.allowed + assert instance.reason == "Content is safe" + + +def test_load_yaml_guardrailresult(): + yaml_data = r""" + allowed: true + reason: Content is safe + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = GuardrailResult.load(data) + assert instance is not None + assert instance.allowed + assert instance.reason == "Content is safe" + + +def test_roundtrip_json_guardrailresult(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "allowed": true, + "reason": "Content is safe" + } + """ + original_data = json.loads(json_data, strict=False) + instance = GuardrailResult.load(original_data) + saved_data = instance.save() + reloaded = GuardrailResult.load(saved_data) + assert reloaded is not None + assert reloaded.allowed + assert reloaded.reason == "Content is safe" + + +def test_to_json_guardrailresult(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "allowed": true, + "reason": "Content is safe" + } + """ + data = json.loads(json_data, strict=False) + instance = GuardrailResult.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_guardrailresult(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "allowed": true, + "reason": "Content is safe" + } + """ + data = json.loads(json_data, strict=False) + instance = GuardrailResult.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) + + +def test_factory_rewrite_guardrailresult(): + """Test that rewrite() factory creates a valid instance.""" + instance = GuardrailResult.create_rewrite("test") + assert instance is not None + assert isinstance(instance, GuardrailResult) + assert instance.allowed + + +def test_factory_deny_guardrailresult(): + """Test that deny() factory creates a valid instance.""" + instance = GuardrailResult.deny("test") + assert instance is not None + assert isinstance(instance, GuardrailResult) + assert not instance.allowed + + +def test_factory_allow_guardrailresult(): + """Test that allow() factory creates a valid instance.""" + instance = GuardrailResult.allow() + assert instance is not None + assert isinstance(instance, GuardrailResult) + assert instance.allowed diff --git a/runtime/python/prompty/tests/model/test_image_part.py b/runtime/python/prompty/tests/model/test_image_part.py new file mode 100644 index 00000000..97e53a92 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_image_part.py @@ -0,0 +1,89 @@ +import json + +import yaml + +from prompty.model import ImagePart + + +def test_load_json_imagepart(): + json_data = r""" + { + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" + } + """ + data = json.loads(json_data, strict=False) + instance = ImagePart.load(data) + assert instance is not None + assert instance.source == "https://example.com/image.png" + assert instance.detail == "auto" + assert instance.media_type == "image/png" + + +def test_load_yaml_imagepart(): + yaml_data = r""" + source: "https://example.com/image.png" + detail: auto + mediaType: image/png + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ImagePart.load(data) + assert instance is not None + assert instance.source == "https://example.com/image.png" + assert instance.detail == "auto" + assert instance.media_type == "image/png" + + +def test_roundtrip_json_imagepart(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" + } + """ + original_data = json.loads(json_data, strict=False) + instance = ImagePart.load(original_data) + saved_data = instance.save() + reloaded = ImagePart.load(saved_data) + assert reloaded is not None + assert reloaded.source == "https://example.com/image.png" + assert reloaded.detail == "auto" + assert reloaded.media_type == "image/png" + + +def test_to_json_imagepart(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" + } + """ + data = json.loads(json_data, strict=False) + instance = ImagePart.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_imagepart(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" + } + """ + data = json.loads(json_data, strict=False) + instance = ImagePart.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_mcp_tool.py b/runtime/python/prompty/tests/model/test_mcp_tool.py index ca3248fa..29583f1b 100644 --- a/runtime/python/prompty/tests/model/test_mcp_tool.py +++ b/runtime/python/prompty/tests/model/test_mcp_tool.py @@ -27,8 +27,8 @@ def test_load_json_mcptool(): instance = McpTool.load(data) assert instance is not None assert instance.kind == "mcp" - assert instance.serverName == "My MCP Server" - assert instance.serverDescription == "This tool allows access to MCP services." + assert instance.server_name == "My MCP Server" + assert instance.server_description == "This tool allows access to MCP services." def test_load_yaml_mcptool(): @@ -49,8 +49,8 @@ def test_load_yaml_mcptool(): instance = McpTool.load(data) assert instance is not None assert instance.kind == "mcp" - assert instance.serverName == "My MCP Server" - assert instance.serverDescription == "This tool allows access to MCP services." + assert instance.server_name == "My MCP Server" + assert instance.server_description == "This tool allows access to MCP services." def test_roundtrip_json_mcptool(): @@ -78,8 +78,8 @@ def test_roundtrip_json_mcptool(): reloaded = McpTool.load(saved_data) assert reloaded is not None assert reloaded.kind == "mcp" - assert reloaded.serverName == "My MCP Server" - assert reloaded.serverDescription == "This tool allows access to MCP services." + assert reloaded.server_name == "My MCP Server" + assert reloaded.server_description == "This tool allows access to MCP services." def test_to_json_mcptool(): diff --git a/runtime/python/prompty/tests/model/test_message.py b/runtime/python/prompty/tests/model/test_message.py new file mode 100644 index 00000000..ac52230a --- /dev/null +++ b/runtime/python/prompty/tests/model/test_message.py @@ -0,0 +1,138 @@ +import json + +import yaml + +from prompty.model import Message + + +def test_load_json_message(): + json_data = r""" + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + """ + data = json.loads(json_data, strict=False) + instance = Message.load(data) + assert instance is not None + assert instance.role == "user" + + +def test_load_yaml_message(): + yaml_data = r""" + role: user + parts: + - kind: text + value: Hello! + metadata: + source: user-input + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = Message.load(data) + assert instance is not None + assert instance.role == "user" + + +def test_roundtrip_json_message(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = Message.load(original_data) + saved_data = instance.save() + reloaded = Message.load(saved_data) + assert reloaded is not None + assert reloaded.role == "user" + + +def test_to_json_message(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + """ + data = json.loads(json_data, strict=False) + instance = Message.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_message(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + """ + data = json.loads(json_data, strict=False) + instance = Message.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) + + +def test_factory_assistant_message(): + """Test that assistant() factory creates a valid instance.""" + instance = Message.assistant("test") + assert instance is not None + assert isinstance(instance, Message) + assert instance.role == "assistant" + + +def test_factory_system_message(): + """Test that system() factory creates a valid instance.""" + instance = Message.system("test") + assert instance is not None + assert isinstance(instance, Message) + assert instance.role == "system" + + +def test_factory_user_message(): + """Test that user() factory creates a valid instance.""" + instance = Message.user("test") + assert instance is not None + assert isinstance(instance, Message) + assert instance.role == "user" diff --git a/runtime/python/prompty/tests/model/test_messages_updated_payload.py b/runtime/python/prompty/tests/model/test_messages_updated_payload.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_messages_updated_payload.py @@ -0,0 +1 @@ + diff --git a/runtime/python/prompty/tests/model/test_model.py b/runtime/python/prompty/tests/model/test_model.py index 51fe26d6..d704c191 100644 --- a/runtime/python/prompty/tests/model/test_model.py +++ b/runtime/python/prompty/tests/model/test_model.py @@ -28,7 +28,7 @@ def test_load_json_model(): assert instance is not None assert instance.id == "gpt-35-turbo" assert instance.provider == "foundry" - assert instance.apiType == "chat" + assert instance.api_type == "chat" def test_load_yaml_model(): @@ -51,7 +51,7 @@ def test_load_yaml_model(): assert instance is not None assert instance.id == "gpt-35-turbo" assert instance.provider == "foundry" - assert instance.apiType == "chat" + assert instance.api_type == "chat" def test_roundtrip_json_model(): @@ -80,7 +80,7 @@ def test_roundtrip_json_model(): assert reloaded is not None assert reloaded.id == "gpt-35-turbo" assert reloaded.provider == "foundry" - assert reloaded.apiType == "chat" + assert reloaded.api_type == "chat" def test_to_json_model(): diff --git a/runtime/python/prompty/tests/model/test_model_info.py b/runtime/python/prompty/tests/model/test_model_info.py new file mode 100644 index 00000000..da7b8da7 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_model_info.py @@ -0,0 +1,144 @@ +import json + +import yaml + +from prompty.model import ModelInfo + + +def test_load_json_modelinfo(): + json_data = r""" + { + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } + } + """ + data = json.loads(json_data, strict=False) + instance = ModelInfo.load(data) + assert instance is not None + assert instance.id == "gpt-4o" + assert instance.display_name == "GPT-4o" + assert instance.owned_by == "openai" + assert instance.context_window == 128000 + + +def test_load_yaml_modelinfo(): + yaml_data = r""" + id: gpt-4o + displayName: GPT-4o + ownedBy: openai + contextWindow: 128000 + inputModalities: + - text + - image + outputModalities: + - text + additionalProperties: + supportsStreaming: true + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ModelInfo.load(data) + assert instance is not None + assert instance.id == "gpt-4o" + assert instance.display_name == "GPT-4o" + assert instance.owned_by == "openai" + assert instance.context_window == 128000 + + +def test_roundtrip_json_modelinfo(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = ModelInfo.load(original_data) + saved_data = instance.save() + reloaded = ModelInfo.load(saved_data) + assert reloaded is not None + assert reloaded.id == "gpt-4o" + assert reloaded.display_name == "GPT-4o" + assert reloaded.owned_by == "openai" + assert reloaded.context_window == 128000 + + +def test_to_json_modelinfo(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } + } + """ + data = json.loads(json_data, strict=False) + instance = ModelInfo.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_modelinfo(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } + } + """ + data = json.loads(json_data, strict=False) + instance = ModelInfo.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_model_options.py b/runtime/python/prompty/tests/model/test_model_options.py index 553f863c..37ee578e 100644 --- a/runtime/python/prompty/tests/model/test_model_options.py +++ b/runtime/python/prompty/tests/model/test_model_options.py @@ -29,15 +29,14 @@ def test_load_json_modeloptions(): data = json.loads(json_data, strict=False) instance = ModelOptions.load(data) assert instance is not None - assert instance.frequencyPenalty == 0.5 - assert instance.maxOutputTokens == 2048 - assert instance.presencePenalty == 0.3 + assert instance.frequency_penalty == 0.5 + assert instance.max_output_tokens == 2048 + assert instance.presence_penalty == 0.3 assert instance.seed == 42 assert instance.temperature == 0.7 - assert instance.topK == 40 - assert instance.topP == 0.9 - - assert instance.allowMultipleToolCalls + assert instance.top_k == 40 + assert instance.top_p == 0.9 + assert instance.allow_multiple_tool_calls def test_load_yaml_modeloptions(): @@ -61,14 +60,14 @@ def test_load_yaml_modeloptions(): data = yaml.load(yaml_data, Loader=yaml.FullLoader) instance = ModelOptions.load(data) assert instance is not None - assert instance.frequencyPenalty == 0.5 - assert instance.maxOutputTokens == 2048 - assert instance.presencePenalty == 0.3 + assert instance.frequency_penalty == 0.5 + assert instance.max_output_tokens == 2048 + assert instance.presence_penalty == 0.3 assert instance.seed == 42 assert instance.temperature == 0.7 - assert instance.topK == 40 - assert instance.topP == 0.9 - assert instance.allowMultipleToolCalls + assert instance.top_k == 40 + assert instance.top_p == 0.9 + assert instance.allow_multiple_tool_calls def test_roundtrip_json_modeloptions(): @@ -98,14 +97,14 @@ def test_roundtrip_json_modeloptions(): saved_data = instance.save() reloaded = ModelOptions.load(saved_data) assert reloaded is not None - assert reloaded.frequencyPenalty == 0.5 - assert reloaded.maxOutputTokens == 2048 - assert reloaded.presencePenalty == 0.3 + assert reloaded.frequency_penalty == 0.5 + assert reloaded.max_output_tokens == 2048 + assert reloaded.presence_penalty == 0.3 assert reloaded.seed == 42 assert reloaded.temperature == 0.7 - assert reloaded.topK == 40 - assert reloaded.topP == 0.9 - assert reloaded.allowMultipleToolCalls + assert reloaded.top_k == 40 + assert reloaded.top_p == 0.9 + assert reloaded.allow_multiple_tool_calls def test_to_json_modeloptions(): diff --git a/runtime/python/prompty/tests/model/test_o_auth_connection.py b/runtime/python/prompty/tests/model/test_o_auth_connection.py index cf46b761..d00be0da 100644 --- a/runtime/python/prompty/tests/model/test_o_auth_connection.py +++ b/runtime/python/prompty/tests/model/test_o_auth_connection.py @@ -23,9 +23,9 @@ def test_load_json_oauthconnection(): assert instance is not None assert instance.kind == "oauth" assert instance.endpoint == "https://api.example.com" - assert instance.clientId == "your-client-id" - assert instance.clientSecret == "your-client-secret" - assert instance.tokenUrl == "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" + assert instance.client_id == "your-client-id" + assert instance.client_secret == "your-client-secret" + assert instance.token_url == "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" def test_load_yaml_oauthconnection(): @@ -44,9 +44,9 @@ def test_load_yaml_oauthconnection(): assert instance is not None assert instance.kind == "oauth" assert instance.endpoint == "https://api.example.com" - assert instance.clientId == "your-client-id" - assert instance.clientSecret == "your-client-secret" - assert instance.tokenUrl == "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" + assert instance.client_id == "your-client-id" + assert instance.client_secret == "your-client-secret" + assert instance.token_url == "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" def test_roundtrip_json_oauthconnection(): @@ -70,9 +70,9 @@ def test_roundtrip_json_oauthconnection(): assert reloaded is not None assert reloaded.kind == "oauth" assert reloaded.endpoint == "https://api.example.com" - assert reloaded.clientId == "your-client-id" - assert reloaded.clientSecret == "your-client-secret" - assert reloaded.tokenUrl == "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" + assert reloaded.client_id == "your-client-id" + assert reloaded.client_secret == "your-client-secret" + assert reloaded.token_url == "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" def test_to_json_oauthconnection(): diff --git a/runtime/python/prompty/tests/model/test_prompty.py b/runtime/python/prompty/tests/model/test_prompty.py index f5ecabae..997af6e1 100644 --- a/runtime/python/prompty/tests/model/test_prompty.py +++ b/runtime/python/prompty/tests/model/test_prompty.py @@ -77,7 +77,7 @@ def test_load_json_prompty(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -167,7 +167,7 @@ def test_load_yaml_prompty(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -260,7 +260,7 @@ def test_roundtrip_json_prompty(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -503,7 +503,7 @@ def test_load_json_prompty_1(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -593,7 +593,7 @@ def test_load_yaml_prompty_1(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -685,7 +685,7 @@ def test_roundtrip_json_prompty_1(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -928,7 +928,7 @@ def test_load_json_prompty_2(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -1018,7 +1018,7 @@ def test_load_yaml_prompty_2(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -1112,7 +1112,7 @@ def test_roundtrip_json_prompty_2(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -1358,7 +1358,7 @@ def test_load_json_prompty_3(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -1448,7 +1448,7 @@ def test_load_yaml_prompty_3(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -1541,7 +1541,7 @@ def test_roundtrip_json_prompty_3(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -1788,7 +1788,7 @@ def test_load_json_prompty_4(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -1878,7 +1878,7 @@ def test_load_yaml_prompty_4(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -1974,7 +1974,7 @@ def test_roundtrip_json_prompty_4(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -2226,7 +2226,7 @@ def test_load_json_prompty_5(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -2316,7 +2316,7 @@ def test_load_yaml_prompty_5(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -2411,7 +2411,7 @@ def test_roundtrip_json_prompty_5(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -2663,7 +2663,7 @@ def test_load_json_prompty_6(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -2753,7 +2753,7 @@ def test_load_yaml_prompty_6(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -2850,7 +2850,7 @@ def test_roundtrip_json_prompty_6(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions @@ -3105,7 +3105,7 @@ def test_load_json_prompty_7(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -3195,7 +3195,7 @@ def test_load_yaml_prompty_7(): instance = Prompty.load(data) assert instance is not None assert instance.name == "basic-prompt" - assert instance.displayName == "Basic Prompt" + assert instance.display_name == "Basic Prompt" assert instance.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( instance.instructions @@ -3291,7 +3291,7 @@ def test_roundtrip_json_prompty_7(): reloaded = Prompty.load(saved_data) assert reloaded is not None assert reloaded.name == "basic-prompt" - assert reloaded.displayName == "Basic Prompt" + assert reloaded.display_name == "Basic Prompt" assert reloaded.description == "A basic prompt that uses the GPT-3 chat API to answer questions" assert ( reloaded.instructions diff --git a/runtime/python/prompty/tests/model/test_property.py b/runtime/python/prompty/tests/model/test_property.py index 25fa9ac1..52a0e219 100644 --- a/runtime/python/prompty/tests/model/test_property.py +++ b/runtime/python/prompty/tests/model/test_property.py @@ -27,7 +27,6 @@ def test_load_json_property(): assert instance.name == "my-input" assert instance.kind == "string" assert instance.description == "A description of the input property" - assert instance.required assert instance.default == "default value" assert instance.example == "example value" diff --git a/runtime/python/prompty/tests/model/test_status_event_payload.py b/runtime/python/prompty/tests/model/test_status_event_payload.py new file mode 100644 index 00000000..4fd24528 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_status_event_payload.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import StatusEventPayload + + +def test_load_json_statuseventpayload(): + json_data = r""" + { + "message": "Starting iteration 3" + } + """ + data = json.loads(json_data, strict=False) + instance = StatusEventPayload.load(data) + assert instance is not None + assert instance.message == "Starting iteration 3" + + +def test_load_yaml_statuseventpayload(): + yaml_data = r""" + message: Starting iteration 3 + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = StatusEventPayload.load(data) + assert instance is not None + assert instance.message == "Starting iteration 3" + + +def test_roundtrip_json_statuseventpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "message": "Starting iteration 3" + } + """ + original_data = json.loads(json_data, strict=False) + instance = StatusEventPayload.load(original_data) + saved_data = instance.save() + reloaded = StatusEventPayload.load(saved_data) + assert reloaded is not None + assert reloaded.message == "Starting iteration 3" + + +def test_to_json_statuseventpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "message": "Starting iteration 3" + } + """ + data = json.loads(json_data, strict=False) + instance = StatusEventPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_statuseventpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "message": "Starting iteration 3" + } + """ + data = json.loads(json_data, strict=False) + instance = StatusEventPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_stream_chunk.py b/runtime/python/prompty/tests/model/test_stream_chunk.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_stream_chunk.py @@ -0,0 +1 @@ + diff --git a/runtime/python/prompty/tests/model/test_text_chunk.py b/runtime/python/prompty/tests/model/test_text_chunk.py new file mode 100644 index 00000000..54e5bc68 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_text_chunk.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import TextChunk + + +def test_load_json_textchunk(): + json_data = r""" + { + "value": "Hello" + } + """ + data = json.loads(json_data, strict=False) + instance = TextChunk.load(data) + assert instance is not None + assert instance.value == "Hello" + + +def test_load_yaml_textchunk(): + yaml_data = r""" + value: Hello + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = TextChunk.load(data) + assert instance is not None + assert instance.value == "Hello" + + +def test_roundtrip_json_textchunk(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "value": "Hello" + } + """ + original_data = json.loads(json_data, strict=False) + instance = TextChunk.load(original_data) + saved_data = instance.save() + reloaded = TextChunk.load(saved_data) + assert reloaded is not None + assert reloaded.value == "Hello" + + +def test_to_json_textchunk(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "value": "Hello" + } + """ + data = json.loads(json_data, strict=False) + instance = TextChunk.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_textchunk(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "value": "Hello" + } + """ + data = json.loads(json_data, strict=False) + instance = TextChunk.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_text_part.py b/runtime/python/prompty/tests/model/test_text_part.py new file mode 100644 index 00000000..b8d22986 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_text_part.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import TextPart + + +def test_load_json_textpart(): + json_data = r""" + { + "value": "Hello, world!" + } + """ + data = json.loads(json_data, strict=False) + instance = TextPart.load(data) + assert instance is not None + assert instance.value == "Hello, world!" + + +def test_load_yaml_textpart(): + yaml_data = r""" + value: Hello, world! + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = TextPart.load(data) + assert instance is not None + assert instance.value == "Hello, world!" + + +def test_roundtrip_json_textpart(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "value": "Hello, world!" + } + """ + original_data = json.loads(json_data, strict=False) + instance = TextPart.load(original_data) + saved_data = instance.save() + reloaded = TextPart.load(saved_data) + assert reloaded is not None + assert reloaded.value == "Hello, world!" + + +def test_to_json_textpart(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "value": "Hello, world!" + } + """ + data = json.loads(json_data, strict=False) + instance = TextPart.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_textpart(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "value": "Hello, world!" + } + """ + data = json.loads(json_data, strict=False) + instance = TextPart.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_thinking_chunk.py b/runtime/python/prompty/tests/model/test_thinking_chunk.py new file mode 100644 index 00000000..de449d7d --- /dev/null +++ b/runtime/python/prompty/tests/model/test_thinking_chunk.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import ThinkingChunk + + +def test_load_json_thinkingchunk(): + json_data = r""" + { + "value": "Let me consider..." + } + """ + data = json.loads(json_data, strict=False) + instance = ThinkingChunk.load(data) + assert instance is not None + assert instance.value == "Let me consider..." + + +def test_load_yaml_thinkingchunk(): + yaml_data = r""" + value: Let me consider... + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ThinkingChunk.load(data) + assert instance is not None + assert instance.value == "Let me consider..." + + +def test_roundtrip_json_thinkingchunk(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "value": "Let me consider..." + } + """ + original_data = json.loads(json_data, strict=False) + instance = ThinkingChunk.load(original_data) + saved_data = instance.save() + reloaded = ThinkingChunk.load(saved_data) + assert reloaded is not None + assert reloaded.value == "Let me consider..." + + +def test_to_json_thinkingchunk(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "value": "Let me consider..." + } + """ + data = json.loads(json_data, strict=False) + instance = ThinkingChunk.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_thinkingchunk(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "value": "Let me consider..." + } + """ + data = json.loads(json_data, strict=False) + instance = ThinkingChunk.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_thinking_event_payload.py b/runtime/python/prompty/tests/model/test_thinking_event_payload.py new file mode 100644 index 00000000..06b5e6c9 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_thinking_event_payload.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import ThinkingEventPayload + + +def test_load_json_thinkingeventpayload(): + json_data = r""" + { + "token": "Let me consider..." + } + """ + data = json.loads(json_data, strict=False) + instance = ThinkingEventPayload.load(data) + assert instance is not None + assert instance.token == "Let me consider..." + + +def test_load_yaml_thinkingeventpayload(): + yaml_data = r""" + token: Let me consider... + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ThinkingEventPayload.load(data) + assert instance is not None + assert instance.token == "Let me consider..." + + +def test_roundtrip_json_thinkingeventpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "token": "Let me consider..." + } + """ + original_data = json.loads(json_data, strict=False) + instance = ThinkingEventPayload.load(original_data) + saved_data = instance.save() + reloaded = ThinkingEventPayload.load(saved_data) + assert reloaded is not None + assert reloaded.token == "Let me consider..." + + +def test_to_json_thinkingeventpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "token": "Let me consider..." + } + """ + data = json.loads(json_data, strict=False) + instance = ThinkingEventPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_thinkingeventpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "token": "Let me consider..." + } + """ + data = json.loads(json_data, strict=False) + instance = ThinkingEventPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_thread_marker.py b/runtime/python/prompty/tests/model/test_thread_marker.py new file mode 100644 index 00000000..b9a9b20a --- /dev/null +++ b/runtime/python/prompty/tests/model/test_thread_marker.py @@ -0,0 +1,81 @@ +import json + +import yaml + +from prompty.model import ThreadMarker + + +def test_load_json_threadmarker(): + json_data = r""" + { + "name": "thread", + "kind": "thread" + } + """ + data = json.loads(json_data, strict=False) + instance = ThreadMarker.load(data) + assert instance is not None + assert instance.name == "thread" + assert instance.kind == "thread" + + +def test_load_yaml_threadmarker(): + yaml_data = r""" + name: thread + kind: thread + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ThreadMarker.load(data) + assert instance is not None + assert instance.name == "thread" + assert instance.kind == "thread" + + +def test_roundtrip_json_threadmarker(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "name": "thread", + "kind": "thread" + } + """ + original_data = json.loads(json_data, strict=False) + instance = ThreadMarker.load(original_data) + saved_data = instance.save() + reloaded = ThreadMarker.load(saved_data) + assert reloaded is not None + assert reloaded.name == "thread" + assert reloaded.kind == "thread" + + +def test_to_json_threadmarker(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "name": "thread", + "kind": "thread" + } + """ + data = json.loads(json_data, strict=False) + instance = ThreadMarker.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_threadmarker(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "name": "thread", + "kind": "thread" + } + """ + data = json.loads(json_data, strict=False) + instance = ThreadMarker.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_token_event_payload.py b/runtime/python/prompty/tests/model/test_token_event_payload.py new file mode 100644 index 00000000..e0e566ef --- /dev/null +++ b/runtime/python/prompty/tests/model/test_token_event_payload.py @@ -0,0 +1,73 @@ +import json + +import yaml + +from prompty.model import TokenEventPayload + + +def test_load_json_tokeneventpayload(): + json_data = r""" + { + "token": "Hello" + } + """ + data = json.loads(json_data, strict=False) + instance = TokenEventPayload.load(data) + assert instance is not None + assert instance.token == "Hello" + + +def test_load_yaml_tokeneventpayload(): + yaml_data = r""" + token: Hello + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = TokenEventPayload.load(data) + assert instance is not None + assert instance.token == "Hello" + + +def test_roundtrip_json_tokeneventpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "token": "Hello" + } + """ + original_data = json.loads(json_data, strict=False) + instance = TokenEventPayload.load(original_data) + saved_data = instance.save() + reloaded = TokenEventPayload.load(saved_data) + assert reloaded is not None + assert reloaded.token == "Hello" + + +def test_to_json_tokeneventpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "token": "Hello" + } + """ + data = json.loads(json_data, strict=False) + instance = TokenEventPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_tokeneventpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "token": "Hello" + } + """ + data = json.loads(json_data, strict=False) + instance = TokenEventPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_token_usage.py b/runtime/python/prompty/tests/model/test_token_usage.py new file mode 100644 index 00000000..458c67ae --- /dev/null +++ b/runtime/python/prompty/tests/model/test_token_usage.py @@ -0,0 +1,89 @@ +import json + +import yaml + +from prompty.model import TokenUsage + + +def test_load_json_tokenusage(): + json_data = r""" + { + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 + } + """ + data = json.loads(json_data, strict=False) + instance = TokenUsage.load(data) + assert instance is not None + assert instance.prompt_tokens == 150 + assert instance.completion_tokens == 42 + assert instance.total_tokens == 192 + + +def test_load_yaml_tokenusage(): + yaml_data = r""" + promptTokens: 150 + completionTokens: 42 + totalTokens: 192 + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = TokenUsage.load(data) + assert instance is not None + assert instance.prompt_tokens == 150 + assert instance.completion_tokens == 42 + assert instance.total_tokens == 192 + + +def test_roundtrip_json_tokenusage(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 + } + """ + original_data = json.loads(json_data, strict=False) + instance = TokenUsage.load(original_data) + saved_data = instance.save() + reloaded = TokenUsage.load(saved_data) + assert reloaded is not None + assert reloaded.prompt_tokens == 150 + assert reloaded.completion_tokens == 42 + assert reloaded.total_tokens == 192 + + +def test_to_json_tokenusage(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 + } + """ + data = json.loads(json_data, strict=False) + instance = TokenUsage.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_tokenusage(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 + } + """ + data = json.loads(json_data, strict=False) + instance = TokenUsage.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_tool_call.py b/runtime/python/prompty/tests/model/test_tool_call.py new file mode 100644 index 00000000..a7248f51 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_call.py @@ -0,0 +1,89 @@ +import json + +import yaml + +from prompty.model import ToolCall + + +def test_load_json_toolcall(): + json_data = r""" + { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + data = json.loads(json_data, strict=False) + instance = ToolCall.load(data) + assert instance is not None + assert instance.id == "call_abc123" + assert instance.name == "get_weather" + assert instance.arguments == '{"city": "Paris"}' + + +def test_load_yaml_toolcall(): + yaml_data = r""" + id: call_abc123 + name: get_weather + arguments: "{\"city\": \"Paris\"}" + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolCall.load(data) + assert instance is not None + assert instance.id == "call_abc123" + assert instance.name == "get_weather" + assert instance.arguments == '{"city": "Paris"}' + + +def test_roundtrip_json_toolcall(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolCall.load(original_data) + saved_data = instance.save() + reloaded = ToolCall.load(saved_data) + assert reloaded is not None + assert reloaded.id == "call_abc123" + assert reloaded.name == "get_weather" + assert reloaded.arguments == '{"city": "Paris"}' + + +def test_to_json_toolcall(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + data = json.loads(json_data, strict=False) + instance = ToolCall.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolcall(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + data = json.loads(json_data, strict=False) + instance = ToolCall.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_tool_call_start_payload.py b/runtime/python/prompty/tests/model/test_tool_call_start_payload.py new file mode 100644 index 00000000..41d57fe5 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_call_start_payload.py @@ -0,0 +1,81 @@ +import json + +import yaml + +from prompty.model import ToolCallStartPayload + + +def test_load_json_toolcallstartpayload(): + json_data = r""" + { + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + data = json.loads(json_data, strict=False) + instance = ToolCallStartPayload.load(data) + assert instance is not None + assert instance.name == "get_weather" + assert instance.arguments == '{"city": "Paris"}' + + +def test_load_yaml_toolcallstartpayload(): + yaml_data = r""" + name: get_weather + arguments: "{\"city\": \"Paris\"}" + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolCallStartPayload.load(data) + assert instance is not None + assert instance.name == "get_weather" + assert instance.arguments == '{"city": "Paris"}' + + +def test_roundtrip_json_toolcallstartpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolCallStartPayload.load(original_data) + saved_data = instance.save() + reloaded = ToolCallStartPayload.load(saved_data) + assert reloaded is not None + assert reloaded.name == "get_weather" + assert reloaded.arguments == '{"city": "Paris"}' + + +def test_to_json_toolcallstartpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + data = json.loads(json_data, strict=False) + instance = ToolCallStartPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolcallstartpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + """ + data = json.loads(json_data, strict=False) + instance = ToolCallStartPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_tool_chunk.py b/runtime/python/prompty/tests/model/test_tool_chunk.py new file mode 100644 index 00000000..670e143c --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_chunk.py @@ -0,0 +1,89 @@ +import json + +import yaml + +from prompty.model import ToolChunk + + +def test_load_json_toolchunk(): + json_data = r""" + { + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolChunk.load(data) + assert instance is not None + + +def test_load_yaml_toolchunk(): + yaml_data = r""" + toolCall: + id: call_abc123 + name: get_weather + arguments: "{\"city\": \"Paris\"}" + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolChunk.load(data) + assert instance is not None + + +def test_roundtrip_json_toolchunk(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolChunk.load(original_data) + saved_data = instance.save() + reloaded = ToolChunk.load(saved_data) + assert reloaded is not None + + +def test_to_json_toolchunk(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolChunk.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolchunk(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\"city\": \"Paris\"}" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolChunk.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_tool_context.py b/runtime/python/prompty/tests/model/test_tool_context.py new file mode 100644 index 00000000..4a7c8552 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_context.py @@ -0,0 +1,79 @@ +import json + +import yaml + +from prompty.model import ToolContext + + +def test_load_json_toolcontext(): + json_data = r""" + { + "metadata": { + "userId": "user-123" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolContext.load(data) + assert instance is not None + + +def test_load_yaml_toolcontext(): + yaml_data = r""" + metadata: + userId: user-123 + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolContext.load(data) + assert instance is not None + + +def test_roundtrip_json_toolcontext(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "metadata": { + "userId": "user-123" + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolContext.load(original_data) + saved_data = instance.save() + reloaded = ToolContext.load(saved_data) + assert reloaded is not None + + +def test_to_json_toolcontext(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "metadata": { + "userId": "user-123" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolContext.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolcontext(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "metadata": { + "userId": "user-123" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolContext.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_tool_dispatch_result.py b/runtime/python/prompty/tests/model/test_tool_dispatch_result.py new file mode 100644 index 00000000..0d8f7ca6 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_dispatch_result.py @@ -0,0 +1,117 @@ +import json + +import yaml + +from prompty.model import ToolDispatchResult + + +def test_load_json_tooldispatchresult(): + json_data = r""" + { + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolDispatchResult.load(data) + assert instance is not None + assert instance.tool_call_id == "call_abc123" + assert instance.name == "get_weather" + + +def test_load_yaml_tooldispatchresult(): + yaml_data = r""" + toolCallId: call_abc123 + name: get_weather + result: + parts: + - kind: text + value: 72°F and sunny + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolDispatchResult.load(data) + assert instance is not None + assert instance.tool_call_id == "call_abc123" + assert instance.name == "get_weather" + + +def test_roundtrip_json_tooldispatchresult(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolDispatchResult.load(original_data) + saved_data = instance.save() + reloaded = ToolDispatchResult.load(saved_data) + assert reloaded is not None + assert reloaded.tool_call_id == "call_abc123" + assert reloaded.name == "get_weather" + + +def test_to_json_tooldispatchresult(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolDispatchResult.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_tooldispatchresult(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolDispatchResult.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_tool_result.py b/runtime/python/prompty/tests/model/test_tool_result.py new file mode 100644 index 00000000..1717e47d --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_result.py @@ -0,0 +1,99 @@ +import json + +import yaml + +from prompty.model import ToolResult + + +def test_load_json_toolresult(): + json_data = r""" + { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + """ + data = json.loads(json_data, strict=False) + instance = ToolResult.load(data) + assert instance is not None + + +def test_load_yaml_toolresult(): + yaml_data = r""" + parts: + - kind: text + value: 72°F and sunny + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolResult.load(data) + assert instance is not None + + +def test_roundtrip_json_toolresult(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolResult.load(original_data) + saved_data = instance.save() + reloaded = ToolResult.load(saved_data) + assert reloaded is not None + + +def test_to_json_toolresult(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + """ + data = json.loads(json_data, strict=False) + instance = ToolResult.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolresult(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + """ + data = json.loads(json_data, strict=False) + instance = ToolResult.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) + + +def test_factory_text_toolresult(): + """Test that text() factory creates a valid instance.""" + instance = ToolResult.text("test") + assert instance is not None + assert isinstance(instance, ToolResult) diff --git a/runtime/python/prompty/tests/model/test_tool_result_payload.py b/runtime/python/prompty/tests/model/test_tool_result_payload.py new file mode 100644 index 00000000..31eac0d1 --- /dev/null +++ b/runtime/python/prompty/tests/model/test_tool_result_payload.py @@ -0,0 +1,109 @@ +import json + +import yaml + +from prompty.model import ToolResultPayload + + +def test_load_json_toolresultpayload(): + json_data = r""" + { + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolResultPayload.load(data) + assert instance is not None + assert instance.name == "get_weather" + + +def test_load_yaml_toolresultpayload(): + yaml_data = r""" + name: get_weather + result: + parts: + - kind: text + value: 72°F and sunny + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolResultPayload.load(data) + assert instance is not None + assert instance.name == "get_weather" + + +def test_roundtrip_json_toolresultpayload(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolResultPayload.load(original_data) + saved_data = instance.save() + reloaded = ToolResultPayload.load(saved_data) + assert reloaded is not None + assert reloaded.name == "get_weather" + + +def test_to_json_toolresultpayload(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolResultPayload.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolresultpayload(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolResultPayload.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/model/test_turn_options.py b/runtime/python/prompty/tests/model/test_turn_options.py new file mode 100644 index 00000000..8045097d --- /dev/null +++ b/runtime/python/prompty/tests/model/test_turn_options.py @@ -0,0 +1,127 @@ +import json + +import yaml + +from prompty.model import TurnOptions + + +def test_load_json_turnoptions(): + json_data = r""" + { + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } + } + """ + data = json.loads(json_data, strict=False) + instance = TurnOptions.load(data) + assert instance is not None + assert instance.max_iterations == 10 + assert instance.max_llm_retries == 3 + assert instance.context_budget == 100000 + assert instance.parallel_tool_calls + assert not instance.raw + assert instance.turn == 1 + + +def test_load_yaml_turnoptions(): + yaml_data = r""" + maxIterations: 10 + maxLlmRetries: 3 + contextBudget: 100000 + parallelToolCalls: true + raw: false + turn: 1 + compaction: + strategy: summarize + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = TurnOptions.load(data) + assert instance is not None + assert instance.max_iterations == 10 + assert instance.max_llm_retries == 3 + assert instance.context_budget == 100000 + assert instance.parallel_tool_calls + assert not instance.raw + assert instance.turn == 1 + + +def test_roundtrip_json_turnoptions(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = TurnOptions.load(original_data) + saved_data = instance.save() + reloaded = TurnOptions.load(saved_data) + assert reloaded is not None + assert reloaded.max_iterations == 10 + assert reloaded.max_llm_retries == 3 + assert reloaded.context_budget == 100000 + assert reloaded.parallel_tool_calls + assert not reloaded.raw + assert reloaded.turn == 1 + + +def test_to_json_turnoptions(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } + } + """ + data = json.loads(json_data, strict=False) + instance = TurnOptions.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_turnoptions(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } + } + """ + data = json.loads(json_data, strict=False) + instance = TurnOptions.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/prompty/tests/test_anthropic.py b/runtime/python/prompty/tests/test_anthropic.py index 1023bf9c..136ab6f4 100644 --- a/runtime/python/prompty/tests/test_anthropic.py +++ b/runtime/python/prompty/tests/test_anthropic.py @@ -570,7 +570,7 @@ def test_load_chat_prompty(self): agent = load(PROMPTS_DIR / "anthropic_chat.prompty") assert agent.model.provider == "anthropic" - assert agent.model.apiType == "chat" + assert agent.model.api_type == "chat" assert agent.model.id == "claude-sonnet-4-5-20250929" def test_load_tools_prompty(self): diff --git a/runtime/python/prompty/tests/test_loader.py b/runtime/python/prompty/tests/test_loader.py index d4a8cecd..0689593b 100644 --- a/runtime/python/prompty/tests/test_loader.py +++ b/runtime/python/prompty/tests/test_loader.py @@ -117,7 +117,7 @@ def test_load_model_full(self): agent = load(PROMPTS / "basic.prompty") assert agent.model.id == "gpt-4" assert agent.model.provider == "foundry" - assert agent.model.apiType == "chat" + assert agent.model.api_type == "chat" finally: del os.environ["AZURE_OPENAI_ENDPOINT"] del os.environ["AZURE_OPENAI_API_KEY"] @@ -131,7 +131,7 @@ def test_load_model_connection(self): conn = agent.model.connection assert isinstance(conn, ApiKeyConnection) assert conn.endpoint == "https://test.openai.azure.com/" - assert conn.apiKey == "test-key-123" + assert conn.api_key == "test-key-123" finally: del os.environ["AZURE_OPENAI_ENDPOINT"] del os.environ["AZURE_OPENAI_API_KEY"] @@ -145,7 +145,7 @@ def test_load_model_options(self): opts = agent.model.options assert opts is not None assert opts.temperature == 0.7 - assert opts.maxOutputTokens == 1000 + assert opts.max_output_tokens == 1000 finally: del os.environ["AZURE_OPENAI_ENDPOINT"] del os.environ["AZURE_OPENAI_API_KEY"] @@ -221,7 +221,7 @@ def test_load_tools_mcp(self): tool = agent.tools[0] assert isinstance(tool, McpTool) assert tool.name == "filesystem" - assert tool.serverName == "filesystem-server" + assert tool.server_name == "filesystem-server" assert isinstance(tool.connection, ReferenceConnection) def test_load_tools_openapi(self): @@ -300,7 +300,7 @@ def test_load_env_resolution(self): conn = agent.model.connection assert isinstance(conn, ApiKeyConnection) assert conn.endpoint == "https://resolved.openai.azure.com/" - assert conn.apiKey == "resolved-key" + assert conn.api_key == "resolved-key" finally: del os.environ["TEST_ENDPOINT"] del os.environ["TEST_API_KEY"] @@ -356,7 +356,7 @@ def test_load_shared_connection(self): conn = agent.model.connection assert isinstance(conn, ApiKeyConnection) assert conn.endpoint == "https://shared.openai.azure.com/" - assert conn.apiKey == "shared-key" + assert conn.api_key == "shared-key" def test_load_shared_connection_kind(self): """Shared connection resolves to correct kind.""" diff --git a/runtime/python/prompty/tests/test_spec_vectors.py b/runtime/python/prompty/tests/test_spec_vectors.py index a941acd0..29eba1df 100644 --- a/runtime/python/prompty/tests/test_spec_vectors.py +++ b/runtime/python/prompty/tests/test_spec_vectors.py @@ -406,8 +406,8 @@ def _check_model(model: Model, expected: dict, errors: list[str]): if model.provider != expected["provider"]: errors.append(f" model.provider: {model.provider!r} != expected {expected['provider']!r}") if "apiType" in expected: - if model.apiType != expected["apiType"]: - errors.append(f" model.apiType: {model.apiType!r} != expected {expected['apiType']!r}") + if model.api_type != expected["apiType"]: + errors.append(f" model.api_type: {model.api_type!r} != expected {expected['apiType']!r}") if "connection" in expected and expected["connection"] is not None: conn = model.connection exp_conn = expected["connection"] @@ -423,9 +423,9 @@ def _check_model(model: Model, expected: dict, errors: list[str]): if actual_ep != exp_conn["endpoint"]: errors.append(f" model.connection.endpoint: {actual_ep!r} != expected {exp_conn['endpoint']!r}") if "apiKey" in exp_conn: - actual_key = getattr(conn, "apiKey", None) + actual_key = getattr(conn, "api_key", None) if actual_key != exp_conn["apiKey"]: - errors.append(f" model.connection.apiKey: {actual_key!r} != expected {exp_conn['apiKey']!r}") + errors.append(f" model.connection.api_key: {actual_key!r} != expected {exp_conn['apiKey']!r}") if "options" in expected and expected["options"] is not None: opts = model.options exp_opts = expected["options"] @@ -434,9 +434,9 @@ def _check_model(model: Model, expected: dict, errors: list[str]): else: if "temperature" in exp_opts and opts.temperature != exp_opts["temperature"]: errors.append(f" model.options.temperature: {opts.temperature} != {exp_opts['temperature']}") - if "maxOutputTokens" in exp_opts and opts.maxOutputTokens != exp_opts["maxOutputTokens"]: + if "maxOutputTokens" in exp_opts and opts.max_output_tokens != exp_opts["maxOutputTokens"]: errors.append( - f" model.options.maxOutputTokens: {opts.maxOutputTokens} != {exp_opts['maxOutputTokens']}" + f" model.options.max_output_tokens: {opts.max_output_tokens} != {exp_opts['maxOutputTokens']}" ) @@ -478,9 +478,9 @@ def _check_tools(actual: list, expected: list[dict], errors: list[str]): act_params = getattr(act, "parameters", []) or [] _check_properties(act_params, exp["parameters"], f"{prefix}.parameters", errors) if "serverName" in exp: - act_val = getattr(act, "serverName", None) + act_val = getattr(act, "server_name", None) if act_val != exp["serverName"]: - errors.append(f" {prefix}.serverName: {act_val!r} != expected {exp['serverName']!r}") + errors.append(f" {prefix}.server_name: {act_val!r} != expected {exp['serverName']!r}") if "specification" in exp: act_val = getattr(act, "specification", None) if act_val != exp["specification"]: diff --git a/runtime/python/prompty/tests/test_structured.py b/runtime/python/prompty/tests/test_structured.py index 7966e93c..44f34222 100644 --- a/runtime/python/prompty/tests/test_structured.py +++ b/runtime/python/prompty/tests/test_structured.py @@ -199,7 +199,7 @@ def test_openai_processor_returns_structured_result_on_chat(self): agent = MagicMock() agent.outputs = [MagicMock()] agent.model = MagicMock() - agent.model.apiType = "chat" + agent.model.api_type = "chat" # Create a mock ChatCompletion mock_choice = MagicMock() diff --git a/runtime/rust/prompty-anthropic/src/wire.rs b/runtime/rust/prompty-anthropic/src/wire.rs index 95ebf960..92dbe27a 100644 --- a/runtime/rust/prompty-anthropic/src/wire.rs +++ b/runtime/rust/prompty-anthropic/src/wire.rs @@ -193,28 +193,46 @@ fn part_to_wire(part: &ContentPart) -> Value { // Options // --------------------------------------------------------------------------- +/// Fix f32 precision artifacts in a JSON value. +/// serde_json serializes f32 via f64, causing 0.1 → 0.10000000149011612. +/// Round-trip through f32 display to get a clean decimal representation. +fn fix_f32_value(v: Value) -> Value { + if v.is_f64() { + if let Some(f) = v.as_f64() { + let s = format!("{}", f as f32); + let clean: f64 = s.parse().unwrap_or(f); + return json!(clean); + } + } + v +} + /// Apply model options to the request body. fn apply_options(agent: &Prompty, body: &mut Map) { let mut max_tokens = DEFAULT_MAX_TOKENS; if let Some(opts) = &agent.model.options { - if let Some(v) = opts.temperature { - body.insert("temperature".into(), f32_to_json(v)); - } - if let Some(v) = opts.top_p { - body.insert("top_p".into(), f32_to_json(v)); - } - if let Some(v) = opts.top_k { - body.insert("top_k".into(), json!(v)); - } - if let Some(v) = opts.max_output_tokens { - max_tokens = v as i64; - } - if let Some(ref seqs) = opts.stop_sequences { - body.insert("stop_sequences".into(), json!(seqs)); + let wire = opts.to_wire("anthropic"); + if let Value::Object(map) = wire { + for (k, v) in map { + if v.is_null() { + continue; + } + if k == "max_tokens" { + max_tokens = v.as_i64().unwrap_or(DEFAULT_MAX_TOKENS); + } else { + body.insert(k, fix_f32_value(v)); + } + } } - if let Some(v) = opts.seed { - body.insert("seed".into(), json!(v)); + + // additionalProperties — merge any extra keys + if let Some(map) = opts.additional_properties.as_object() { + for (k, v) in map { + if !body.contains_key(k) { + body.insert(k.clone(), v.clone()); + } + } } } @@ -222,14 +240,6 @@ fn apply_options(agent: &Prompty, body: &mut Map) { body.insert("max_tokens".into(), json!(max_tokens)); } -/// Convert f32 to JSON Value without precision artifacts. -/// f32 0.1 → "0.1" not "0.10000000149011612" -fn f32_to_json(v: f32) -> Value { - let s = format!("{}", v); - let f: f64 = s.parse().unwrap_or(v as f64); - json!(f) -} - // --------------------------------------------------------------------------- // Structured output (outputSchema → output_config) // --------------------------------------------------------------------------- @@ -246,7 +256,7 @@ fn output_schema_to_wire(agent: &Prompty) -> Option { let mut properties = Map::new(); let mut required = Vec::new(); - for prop in &outputs { + for prop in outputs { let kind_str = prop.kind_str(); let json_type = match kind_str { "float" | "number" => "number", @@ -341,16 +351,14 @@ fn property_to_json_schema(prop: &Property) -> Value { } } PropertyKind::Object { properties } => { - if let Some(arr) = properties.as_array() { - let ctx = prompty::model::context::LoadContext::default(); + if !properties.is_empty() { let mut nested = Map::new(); let mut req = Vec::new(); - for val in arr { - let p = Property::load_from_value(val, &ctx); + for p in properties { if p.name.is_empty() { continue; } - nested.insert(p.name.clone(), property_to_json_schema(&p)); + nested.insert(p.name.clone(), property_to_json_schema(p)); req.push(json!(p.name)); } schema.insert("properties".into(), Value::Object(nested)); @@ -367,23 +375,12 @@ fn property_to_json_schema(prop: &Property) -> Value { Value::Object(schema) } -/// Convert tool parameters (stored as serde_json::Value) to JSON Schema for `input_schema`. -fn parameters_to_json_schema(params_value: &Value) -> Value { - use prompty::model::context::LoadContext; - - let ctx = LoadContext::default(); - let params: Vec = if let Some(arr) = params_value.as_array() { - arr.iter() - .map(|v| Property::load_from_value(v, &ctx)) - .collect() - } else { - return json!({"type": "object", "properties": {}}); - }; - +/// Convert tool parameters to JSON Schema for `input_schema`. +fn parameters_to_json_schema(params: &[Property]) -> Value { let mut properties = Map::new(); let mut required = Vec::new(); - for param in ¶ms { + for param in params { properties.insert(param.name.clone(), property_to_json_schema(param)); if param.required.unwrap_or(false) { required.push(json!(param.name)); diff --git a/runtime/rust/prompty-openai/src/wire.rs b/runtime/rust/prompty-openai/src/wire.rs index 12564264..0810590b 100644 --- a/runtime/rust/prompty-openai/src/wire.rs +++ b/runtime/rust/prompty-openai/src/wire.rs @@ -201,38 +201,30 @@ fn extract_text_input(messages: &[Message]) -> Value { // Options mapping // --------------------------------------------------------------------------- -/// Convert f32 to JSON Value without precision artifacts. -/// f32 0.1 → "0.1" not "0.10000000149011612" -fn f32_to_json(v: f32) -> Value { - // Round-trip through string to get clean decimal representation - let s = format!("{}", v); - let f: f64 = s.parse().unwrap_or(v as f64); - json!(f) +/// Fix f32 precision artifacts in a JSON value. +/// serde_json serializes f32 via f64, causing 0.1 → 0.10000000149011612. +/// Round-trip through f32 display to get a clean decimal representation. +fn fix_f32_value(v: Value) -> Value { + if v.is_f64() { + if let Some(f) = v.as_f64() { + let s = format!("{}", f as f32); + let clean: f64 = s.parse().unwrap_or(f); + return json!(clean); + } + } + v } fn apply_options(args: &mut Map, opts: &Option) { let Some(opts) = opts else { return }; - if let Some(t) = opts.temperature { - args.insert("temperature".to_string(), f32_to_json(t)); - } - if let Some(m) = opts.max_output_tokens { - args.insert("max_completion_tokens".to_string(), json!(m)); - } - if let Some(p) = opts.top_p { - args.insert("top_p".to_string(), f32_to_json(p)); - } - if let Some(f) = opts.frequency_penalty { - args.insert("frequency_penalty".to_string(), f32_to_json(f)); - } - if let Some(p) = opts.presence_penalty { - args.insert("presence_penalty".to_string(), f32_to_json(p)); - } - if let Some(s) = opts.seed { - args.insert("seed".to_string(), json!(s)); - } - if let Some(ref stop) = opts.stop_sequences { - args.insert("stop".to_string(), json!(stop)); + let wire = opts.to_wire("openai"); + if let Value::Object(map) = wire { + for (k, v) in map { + if !v.is_null() { + args.insert(k, fix_f32_value(v)); + } + } } // additionalProperties — merge any extra keys @@ -277,19 +269,15 @@ fn function_tool_to_wire(tool: &Tool) -> Value { // Collect bound parameter names to strip from wire format (§7.1.3) let bound_names: std::collections::HashSet = tool - .as_bindings() - .unwrap_or_default() + .bindings .iter() .map(|b| b.name.clone()) .collect(); // Parameters → JSON Schema, filtering out bound params - if let Some(params) = parameters.as_array() { - use prompty::model::context::LoadContext; - let ctx = LoadContext::default(); - let typed_params: Vec = params + { + let typed_params: Vec<&Property> = parameters .iter() - .map(|v| Property::load_from_value(v, &ctx)) .filter(|p| !bound_names.contains(&p.name)) .collect(); let schema = parameters_to_json_schema(&typed_params); @@ -336,23 +324,19 @@ fn property_to_json_schema(prop: &Property) -> Value { // When items is null/unspecified, emit bare {"type": "array"} } PropertyKind::Object { properties } => { - if let Some(arr) = properties.as_array() { - if !arr.is_empty() { - let ctx = prompty::model::context::LoadContext::default(); - let mut nested = Map::new(); - let mut req = Vec::new(); - for val in arr { - let p = Property::load_from_value(val, &ctx); - if p.name.is_empty() { - continue; - } - nested.insert(p.name.clone(), property_to_json_schema(&p)); - req.push(Value::String(p.name.clone())); + if !properties.is_empty() { + let mut nested = Map::new(); + let mut req = Vec::new(); + for p in properties { + if p.name.is_empty() { + continue; } - schema.insert("properties".to_string(), Value::Object(nested)); - schema.insert("required".to_string(), Value::Array(req)); - schema.insert("additionalProperties".to_string(), Value::Bool(false)); + nested.insert(p.name.clone(), property_to_json_schema(p)); + req.push(Value::String(p.name.clone())); } + schema.insert("properties".to_string(), Value::Object(nested)); + schema.insert("required".to_string(), Value::Array(req)); + schema.insert("additionalProperties".to_string(), Value::Bool(false)); } // When properties is empty or absent, emit bare {"type": "object"} } @@ -362,7 +346,7 @@ fn property_to_json_schema(prop: &Property) -> Value { Value::Object(schema) } -fn parameters_to_json_schema(params: &[Property]) -> Value { +fn parameters_to_json_schema(params: &[&Property]) -> Value { let mut properties = Map::new(); let mut required = Vec::new(); @@ -408,7 +392,7 @@ fn output_schema_to_wire(agent: &Prompty) -> Option { let mut properties = Map::new(); let mut required = Vec::new(); - for prop in &outputs { + for prop in outputs { properties.insert(prop.name.clone(), property_to_json_schema(prop)); if prop.required.unwrap_or(false) { required.push(Value::String(prop.name.clone())); @@ -524,14 +508,13 @@ fn message_to_responses_input(msg: &Message) -> Value { fn apply_responses_options(args: &mut Map, opts: &Option) { let Some(opts) = opts else { return }; - if let Some(t) = opts.temperature { - args.insert("temperature".to_string(), f32_to_json(t)); - } - if let Some(m) = opts.max_output_tokens { - args.insert("max_output_tokens".to_string(), json!(m)); - } - if let Some(p) = opts.top_p { - args.insert("top_p".to_string(), f32_to_json(p)); + let wire = opts.to_wire("responses"); + if let Value::Object(map) = wire { + for (k, v) in map { + if !v.is_null() { + args.insert(k, fix_f32_value(v)); + } + } } // additionalProperties — pass through without overwriting @@ -573,18 +556,14 @@ fn responses_function_tool_to_wire(tool: &Tool) -> Value { // Collect bound parameter names to strip (§7.1.3) let bound_names: std::collections::HashSet = tool - .as_bindings() - .unwrap_or_default() + .bindings .iter() .map(|b| b.name.clone()) .collect(); - if let Some(params) = parameters.as_array() { - use prompty::model::context::LoadContext; - let ctx = LoadContext::default(); - let typed_params: Vec = params + { + let typed_params: Vec<&Property> = parameters .iter() - .map(|v| Property::load_from_value(v, &ctx)) .filter(|p| !bound_names.contains(&p.name)) .collect(); let schema = parameters_to_json_schema(&typed_params); @@ -610,7 +589,7 @@ fn output_schema_to_responses_wire(agent: &Prompty) -> Option { let mut properties = Map::new(); let mut required = Vec::new(); - for prop in &outputs { + for prop in outputs { properties.insert(prop.name.clone(), property_to_json_schema(prop)); required.push(Value::String(prop.name.clone())); } diff --git a/runtime/rust/prompty/src/lib.rs b/runtime/rust/prompty/src/lib.rs index 4c34fdc5..5bd8fae9 100644 --- a/runtime/rust/prompty/src/lib.rs +++ b/runtime/rust/prompty/src/lib.rs @@ -41,6 +41,7 @@ pub mod guardrails; pub mod interfaces; pub mod loader; pub mod model; +mod model_ext; pub mod parsers; pub mod pipeline; pub mod prelude; diff --git a/runtime/rust/prompty/src/model/binding.rs b/runtime/rust/prompty/src/model/binding.rs index a103de3e..f791ae73 100644 --- a/runtime/rust/prompty/src/model/binding.rs +++ b/runtime/rust/prompty/src/model/binding.rs @@ -1,5 +1,9 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; /// Represents a binding between an input property and a tool parameter. @@ -36,20 +40,11 @@ impl Binding { let value = ctx.process_input(value.clone()); if let Some(s) = value.as_str() { let value = s.to_string(); - let expansion = serde_json::json!({"input":value}); - return Self::load_from_value(&expansion, ctx); + return Binding { input: value.into(), ..Default::default() }; } Self { - name: value - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - input: value - .get("input") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), + name: value.get("name").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + input: value.get("input").and_then(|v| v.as_str()).unwrap_or_default().to_string(), } } @@ -58,17 +53,12 @@ impl Binding { /// Calls `ctx.process_dict` after serialization. pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); + // Write base fields if !self.name.is_empty() { - result.insert( - "name".to_string(), - serde_json::Value::String(self.name.clone()), - ); + result.insert("name".to_string(), serde_json::Value::String(self.name.clone())); } if !self.input.is_empty() { - result.insert( - "input".to_string(), - serde_json::Value::String(self.input.clone()), - ); + result.insert("input".to_string(), serde_json::Value::String(self.input.clone())); } ctx.process_dict(serde_json::Value::Object(result)) } diff --git a/runtime/rust/prompty/src/model/compaction_complete_payload.rs b/runtime/rust/prompty/src/model/compaction_complete_payload.rs new file mode 100644 index 00000000..878f4e1d --- /dev/null +++ b/runtime/rust/prompty/src/model/compaction_complete_payload.rs @@ -0,0 +1,71 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +/// Payload for "compaction_complete" events — context compaction finished. +#[derive(Debug, Clone, Default)] +pub struct CompactionCompletePayload { + /// Number of messages removed during compaction + pub removed: i32, + /// Number of messages remaining after compaction + pub remaining: i32, +} + +impl CompactionCompletePayload { + /// Create a new CompactionCompletePayload with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load CompactionCompletePayload from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load CompactionCompletePayload from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load CompactionCompletePayload from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + removed: value.get("removed").and_then(|v| v.as_i64()).unwrap_or(0) as i32, + remaining: value.get("remaining").and_then(|v| v.as_i64()).unwrap_or(0) as i32, + } + } + + /// Serialize CompactionCompletePayload to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if self.removed != 0 { + result.insert("removed".to_string(), serde_json::Value::Number(serde_json::Number::from(self.removed))); + } + if self.remaining != 0 { + result.insert("remaining".to_string(), serde_json::Value::Number(serde_json::Number::from(self.remaining))); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize CompactionCompletePayload to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize CompactionCompletePayload to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } +} diff --git a/runtime/rust/prompty/src/model/compaction_config.rs b/runtime/rust/prompty/src/model/compaction_config.rs new file mode 100644 index 00000000..5faf5013 --- /dev/null +++ b/runtime/rust/prompty/src/model/compaction_config.rs @@ -0,0 +1,83 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +/// Configuration for context window compaction. When the message history exceeds the context budget, the compaction strategy is applied to reduce the message list while preserving essential information. +#[derive(Debug, Clone, Default)] +pub struct CompactionConfig { + /// The compaction strategy identifier. Built-in strategies include 'summarize'. Can also be a path to a .prompty file used as the summarization prompt. + pub strategy: Option, + /// Character budget for the compacted context. Overrides TurnOptions.contextBudget when set. + pub budget: Option, + /// Additional strategy-specific options + pub options: serde_json::Value, +} + +impl CompactionConfig { + /// Create a new CompactionConfig with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load CompactionConfig from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load CompactionConfig from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load CompactionConfig from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + strategy: value.get("strategy").and_then(|v| v.as_str()).map(|s| s.to_string()), + budget: value.get("budget").and_then(|v| v.as_i64()).map(|v| v as i32), + options: value.get("options").cloned().unwrap_or(serde_json::Value::Null), + } + } + + /// Serialize CompactionConfig to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if let Some(ref val) = self.strategy { + result.insert("strategy".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(val) = self.budget { + result.insert("budget".to_string(), serde_json::Value::Number(serde_json::Number::from(val))); + } + if !self.options.is_null() { + result.insert("options".to_string(), self.options.clone()); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize CompactionConfig to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize CompactionConfig to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } + /// Returns typed reference to the map if the field is an object. + /// Returns `None` if the field is null or not an object. + pub fn as_options_dict(&self) -> Option<&serde_json::Map> { + self.options.as_object() + } + +} diff --git a/runtime/rust/prompty/src/model/compaction_failed_payload.rs b/runtime/rust/prompty/src/model/compaction_failed_payload.rs new file mode 100644 index 00000000..5d6849ab --- /dev/null +++ b/runtime/rust/prompty/src/model/compaction_failed_payload.rs @@ -0,0 +1,65 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +/// Payload for "compaction_failed" events — compaction could not be completed. +#[derive(Debug, Clone, Default)] +pub struct CompactionFailedPayload { + /// Explanation of why compaction failed + pub message: String, +} + +impl CompactionFailedPayload { + /// Create a new CompactionFailedPayload with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load CompactionFailedPayload from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load CompactionFailedPayload from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load CompactionFailedPayload from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + message: value.get("message").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + } + } + + /// Serialize CompactionFailedPayload to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if !self.message.is_empty() { + result.insert("message".to_string(), serde_json::Value::String(self.message.clone())); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize CompactionFailedPayload to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize CompactionFailedPayload to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } +} diff --git a/runtime/rust/prompty/src/model/connection.rs b/runtime/rust/prompty/src/model/connection.rs index ea568ce6..f75ba1f0 100644 --- a/runtime/rust/prompty/src/model/connection.rs +++ b/runtime/rust/prompty/src/model/connection.rs @@ -1,7 +1,12 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; + /// Variant-specific data for [`Connection`], discriminated by `kind`. #[derive(Debug, Clone)] pub enum ConnectionKind { @@ -31,15 +36,6 @@ pub enum ConnectionKind { /// The endpoint for authenticating with the AI service endpoint: String, }, - /// `kind` = `"foundry"` - Foundry { - /// The Foundry project endpoint URL - endpoint: String, - /// The named connection within the Foundry project - name: Option, - /// The connection type within the Foundry project (e.g., 'model', 'index', 'storage') - connection_type: Option, - }, /// `kind` = `"oauth"` OAuth { /// The endpoint URL for the service @@ -53,6 +49,15 @@ pub enum ConnectionKind { /// OAuth scopes to request scopes: Option>, }, + /// `kind` = `"foundry"` + Foundry { + /// The Foundry project endpoint URL + endpoint: String, + /// The named connection within the Foundry project + name: Option, + /// The connection type within the Foundry project (e.g., 'model', 'index', 'storage') + connection_type: Option, + }, } impl Default for ConnectionKind { @@ -100,101 +105,38 @@ impl Connection { let kind_str = value.get("kind").and_then(|v| v.as_str()).unwrap_or(""); let kind = match kind_str { "reference" => ConnectionKind::Reference { - name: value - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - target: value - .get("target") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), + name: value.get("name").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + target: value.get("target").and_then(|v| v.as_str()).map(|s| s.to_string()), }, "remote" => ConnectionKind::Remote { - name: value - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - endpoint: value - .get("endpoint") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), + name: value.get("name").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + endpoint: value.get("endpoint").and_then(|v| v.as_str()).unwrap_or_default().to_string(), }, "key" => ConnectionKind::ApiKey { - endpoint: value - .get("endpoint") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - api_key: value - .get("apiKey") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), + endpoint: value.get("endpoint").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + api_key: value.get("apiKey").and_then(|v| v.as_str()).unwrap_or_default().to_string(), }, "anonymous" => ConnectionKind::Anonymous { - endpoint: value - .get("endpoint") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - }, - "foundry" => ConnectionKind::Foundry { - endpoint: value - .get("endpoint") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - name: value - .get("name") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - connection_type: value - .get("connectionType") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), + endpoint: value.get("endpoint").and_then(|v| v.as_str()).unwrap_or_default().to_string(), }, "oauth" => ConnectionKind::OAuth { - endpoint: value - .get("endpoint") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - client_id: value - .get("clientId") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - client_secret: value - .get("clientSecret") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - token_url: value - .get("tokenUrl") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - scopes: value.get("scopes").and_then(|v| v.as_array()).map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect() - }), + endpoint: value.get("endpoint").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + client_id: value.get("clientId").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + client_secret: value.get("clientSecret").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + token_url: value.get("tokenUrl").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + scopes: value.get("scopes").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()), + }, + "foundry" => ConnectionKind::Foundry { + endpoint: value.get("endpoint").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + name: value.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()), + connection_type: value.get("connectionType").and_then(|v| v.as_str()).map(|s| s.to_string()), }, _ => ConnectionKind::default(), }; Self { - authentication_mode: value - .get("authenticationMode") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - usage_description: value - .get("usageDescription") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - kind, + authentication_mode: value.get("authenticationMode").and_then(|v| v.as_str()).map(|s| s.to_string()), + usage_description: value.get("usageDescription").and_then(|v| v.as_str()).map(|s| s.to_string()), + kind: kind, } } @@ -205,8 +147,8 @@ impl Connection { ConnectionKind::Remote { .. } => "remote", ConnectionKind::ApiKey { .. } => "key", ConnectionKind::Anonymous { .. } => "anonymous", - ConnectionKind::Foundry { .. } => "foundry", ConnectionKind::OAuth { .. } => "oauth", + ConnectionKind::Foundry { .. } => "foundry", } } @@ -216,26 +158,17 @@ impl Connection { pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); // Write the discriminator - result.insert( - "kind".to_string(), - serde_json::Value::String(self.kind_str().to_string()), - ); + result.insert("kind".to_string(), serde_json::Value::String(self.kind_str().to_string())); // Write base fields if let Some(ref val) = self.authentication_mode { - result.insert( - "authenticationMode".to_string(), - serde_json::Value::String(val.clone()), - ); + result.insert("authenticationMode".to_string(), serde_json::Value::String(val.clone())); } if let Some(ref val) = self.usage_description { - result.insert( - "usageDescription".to_string(), - serde_json::Value::String(val.clone()), - ); + result.insert("usageDescription".to_string(), serde_json::Value::String(val.clone())); } // Write variant-specific fields match &self.kind { - ConnectionKind::Reference { name, target, .. } => { + ConnectionKind::Reference { name, target, .. } => { if !name.is_empty() { result.insert("name".to_string(), serde_json::Value::String(name.clone())); } @@ -243,100 +176,53 @@ impl Connection { result.insert("target".to_string(), serde_json::Value::String(val.clone())); } } - ConnectionKind::Remote { name, endpoint, .. } => { + ConnectionKind::Remote { name, endpoint, .. } => { if !name.is_empty() { result.insert("name".to_string(), serde_json::Value::String(name.clone())); } if !endpoint.is_empty() { - result.insert( - "endpoint".to_string(), - serde_json::Value::String(endpoint.clone()), - ); + result.insert("endpoint".to_string(), serde_json::Value::String(endpoint.clone())); } } - ConnectionKind::ApiKey { - endpoint, api_key, .. - } => { + ConnectionKind::ApiKey { endpoint, api_key, .. } => { if !endpoint.is_empty() { - result.insert( - "endpoint".to_string(), - serde_json::Value::String(endpoint.clone()), - ); + result.insert("endpoint".to_string(), serde_json::Value::String(endpoint.clone())); } if !api_key.is_empty() { - result.insert( - "apiKey".to_string(), - serde_json::Value::String(api_key.clone()), - ); + result.insert("apiKey".to_string(), serde_json::Value::String(api_key.clone())); } } - ConnectionKind::Anonymous { endpoint, .. } => { + ConnectionKind::Anonymous { endpoint, .. } => { if !endpoint.is_empty() { - result.insert( - "endpoint".to_string(), - serde_json::Value::String(endpoint.clone()), - ); + result.insert("endpoint".to_string(), serde_json::Value::String(endpoint.clone())); } } - ConnectionKind::Foundry { - endpoint, - name, - connection_type, - .. - } => { + ConnectionKind::OAuth { endpoint, client_id, client_secret, token_url, scopes, .. } => { if !endpoint.is_empty() { - result.insert( - "endpoint".to_string(), - serde_json::Value::String(endpoint.clone()), - ); - } - if let Some(val) = name { - result.insert("name".to_string(), serde_json::Value::String(val.clone())); - } - if let Some(val) = connection_type { - result.insert( - "connectionType".to_string(), - serde_json::Value::String(val.clone()), - ); - } - } - ConnectionKind::OAuth { - endpoint, - client_id, - client_secret, - token_url, - scopes, - .. - } => { - if !endpoint.is_empty() { - result.insert( - "endpoint".to_string(), - serde_json::Value::String(endpoint.clone()), - ); + result.insert("endpoint".to_string(), serde_json::Value::String(endpoint.clone())); } if !client_id.is_empty() { - result.insert( - "clientId".to_string(), - serde_json::Value::String(client_id.clone()), - ); + result.insert("clientId".to_string(), serde_json::Value::String(client_id.clone())); } if !client_secret.is_empty() { - result.insert( - "clientSecret".to_string(), - serde_json::Value::String(client_secret.clone()), - ); + result.insert("clientSecret".to_string(), serde_json::Value::String(client_secret.clone())); } if !token_url.is_empty() { - result.insert( - "tokenUrl".to_string(), - serde_json::Value::String(token_url.clone()), - ); + result.insert("tokenUrl".to_string(), serde_json::Value::String(token_url.clone())); } if let Some(items) = scopes { - result.insert( - "scopes".to_string(), - serde_json::to_value(items).unwrap_or(serde_json::Value::Null), - ); + result.insert("scopes".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null)); + } + } + ConnectionKind::Foundry { endpoint, name, connection_type, .. } => { + if !endpoint.is_empty() { + result.insert("endpoint".to_string(), serde_json::Value::String(endpoint.clone())); + } + if let Some(val) = name { + result.insert("name".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(val) = connection_type { + result.insert("connectionType".to_string(), serde_json::Value::String(val.clone())); } } } @@ -353,3 +239,9 @@ impl Connection { serde_yaml::to_string(&self.to_value(ctx)) } } + + + + + + diff --git a/runtime/rust/prompty/src/model/content_part.rs b/runtime/rust/prompty/src/model/content_part.rs new file mode 100644 index 00000000..da0977a5 --- /dev/null +++ b/runtime/rust/prompty/src/model/content_part.rs @@ -0,0 +1,174 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + + +/// Variant-specific data for [`ContentPart`], discriminated by `kind`. +#[derive(Debug, Clone)] +pub enum ContentPartKind { + /// `kind` = `"text"` + TextPart { + /// The text content + value: String, + }, + /// `kind` = `"image"` + ImagePart { + /// URL or base64-encoded image data + source: String, + /// Detail level hint for the model (e.g., 'auto', 'low', 'high') + detail: Option, + /// MIME type of the image (e.g., 'image/png') + media_type: Option, + }, + /// `kind` = `"file"` + FilePart { + /// URL or base64-encoded file data + source: String, + /// MIME type of the file (e.g., 'application/pdf') + media_type: Option, + }, + /// `kind` = `"audio"` + AudioPart { + /// URL or base64-encoded audio data + source: String, + /// MIME type of the audio (e.g., 'audio/wav') + media_type: Option, + }, +} + +impl Default for ContentPartKind { + fn default() -> Self { + ContentPartKind::TextPart { + value: String::from(""), + } + } +} +/// A part of a message's content. Content parts are discriminated on the `kind` field and represent the different modalities that can appear in a message. +#[derive(Debug, Clone, Default)] +pub struct ContentPart { + /// Variant-specific data, discriminated by `kind`. + pub kind: ContentPartKind, +} + +impl ContentPart { + /// Create a new ContentPart with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load ContentPart from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load ContentPart from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load ContentPart from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + let kind_str = value.get("kind").and_then(|v| v.as_str()).unwrap_or(""); + let kind = match kind_str { + "text" => ContentPartKind::TextPart { + value: value.get("value").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + }, + "image" => ContentPartKind::ImagePart { + source: value.get("source").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + detail: value.get("detail").and_then(|v| v.as_str()).map(|s| s.to_string()), + media_type: value.get("mediaType").and_then(|v| v.as_str()).map(|s| s.to_string()), + }, + "file" => ContentPartKind::FilePart { + source: value.get("source").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + media_type: value.get("mediaType").and_then(|v| v.as_str()).map(|s| s.to_string()), + }, + "audio" => ContentPartKind::AudioPart { + source: value.get("source").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + media_type: value.get("mediaType").and_then(|v| v.as_str()).map(|s| s.to_string()), + }, + _ => ContentPartKind::default(), + }; + Self { + kind: kind, + } + } + + /// Returns the `kind` discriminator string for this instance. + pub fn kind_str(&self) -> &str { + match &self.kind { + ContentPartKind::TextPart { .. } => "text", + ContentPartKind::ImagePart { .. } => "image", + ContentPartKind::FilePart { .. } => "file", + ContentPartKind::AudioPart { .. } => "audio", + } + } + + /// Serialize ContentPart to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write the discriminator + result.insert("kind".to_string(), serde_json::Value::String(self.kind_str().to_string())); + // Write base fields + // Write variant-specific fields + match &self.kind { + ContentPartKind::TextPart { value, .. } => { + if !value.is_empty() { + result.insert("value".to_string(), serde_json::Value::String(value.clone())); + } + } + ContentPartKind::ImagePart { source, detail, media_type, .. } => { + if !source.is_empty() { + result.insert("source".to_string(), serde_json::Value::String(source.clone())); + } + if let Some(val) = detail { + result.insert("detail".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(val) = media_type { + result.insert("mediaType".to_string(), serde_json::Value::String(val.clone())); + } + } + ContentPartKind::FilePart { source, media_type, .. } => { + if !source.is_empty() { + result.insert("source".to_string(), serde_json::Value::String(source.clone())); + } + if let Some(val) = media_type { + result.insert("mediaType".to_string(), serde_json::Value::String(val.clone())); + } + } + ContentPartKind::AudioPart { source, media_type, .. } => { + if !source.is_empty() { + result.insert("source".to_string(), serde_json::Value::String(source.clone())); + } + if let Some(val) = media_type { + result.insert("mediaType".to_string(), serde_json::Value::String(val.clone())); + } + } + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize ContentPart to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize ContentPart to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } +} + + + + diff --git a/runtime/rust/prompty/src/model/context.rs b/runtime/rust/prompty/src/model/context.rs index 0afa09c6..4103cda3 100644 --- a/runtime/rust/prompty/src/model/context.rs +++ b/runtime/rust/prompty/src/model/context.rs @@ -17,7 +17,6 @@ pub type PostSaveFn = Box serde_json::Value + Send /// /// Provides hooks for pre-processing input data before parsing and /// post-processing output data after instantiation. -#[derive(Default)] pub struct LoadContext { /// Optional callback to transform input data before parsing. pub pre_process: Option, @@ -34,6 +33,15 @@ impl std::fmt::Debug for LoadContext { } } +impl Default for LoadContext { + fn default() -> Self { + Self { + pre_process: None, + post_process: None, + } + } +} + impl LoadContext { /// Create a new empty LoadContext. pub fn new() -> Self { @@ -152,11 +160,7 @@ impl SaveContext { } /// Convert a value to a JSON string. - pub fn to_json( - &self, - data: &serde_json::Value, - indent: bool, - ) -> Result { + pub fn to_json(&self, data: &serde_json::Value, indent: bool) -> Result { if indent { serde_json::to_string_pretty(data) } else { diff --git a/runtime/rust/prompty/src/model/done_event_payload.rs b/runtime/rust/prompty/src/model/done_event_payload.rs new file mode 100644 index 00000000..86a64a4e --- /dev/null +++ b/runtime/rust/prompty/src/model/done_event_payload.rs @@ -0,0 +1,93 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +use super::message::Message; + +/// Payload for "done" events — the agent loop completed successfully. +#[derive(Debug, Clone, Default)] +pub struct DoneEventPayload { + /// The final text response from the LLM + pub response: String, + /// The final conversation state including all messages + pub messages: Vec, +} + +impl DoneEventPayload { + /// Create a new DoneEventPayload with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load DoneEventPayload from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load DoneEventPayload from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load DoneEventPayload from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + response: value.get("response").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + messages: value.get("messages").map(|v| Self::load_messages(v, ctx)).unwrap_or_default(), + } + } + + /// Serialize DoneEventPayload to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if !self.response.is_empty() { + result.insert("response".to_string(), serde_json::Value::String(self.response.clone())); + } + if !self.messages.is_empty() { + result.insert("messages".to_string(), Self::save_messages(&self.messages, ctx)); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize DoneEventPayload to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize DoneEventPayload to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } + + /// Load a collection of Message from a JSON value. + /// Handles both array format `[{...}]`. + fn load_messages(data: &serde_json::Value, ctx: &LoadContext) -> Vec { + match data { + serde_json::Value::Array(arr) => { + arr.iter().map(|v| Message::load_from_value(v, ctx)).collect() + } + + _ => Vec::new(), + + } + } + + /// Save a collection of Message to a JSON value. + fn save_messages(items: &[Message], ctx: &SaveContext) -> serde_json::Value { + + serde_json::Value::Array(items.iter().map(|item| item.to_value(ctx)).collect::>()) + + } +} diff --git a/runtime/rust/prompty/src/model/error_event_payload.rs b/runtime/rust/prompty/src/model/error_event_payload.rs new file mode 100644 index 00000000..0749c533 --- /dev/null +++ b/runtime/rust/prompty/src/model/error_event_payload.rs @@ -0,0 +1,65 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +/// Payload for "error" events — an error occurred during the loop. +#[derive(Debug, Clone, Default)] +pub struct ErrorEventPayload { + /// Human-readable error description + pub message: String, +} + +impl ErrorEventPayload { + /// Create a new ErrorEventPayload with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load ErrorEventPayload from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load ErrorEventPayload from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load ErrorEventPayload from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + message: value.get("message").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + } + } + + /// Serialize ErrorEventPayload to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if !self.message.is_empty() { + result.insert("message".to_string(), serde_json::Value::String(self.message.clone())); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize ErrorEventPayload to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize ErrorEventPayload to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } +} diff --git a/runtime/rust/prompty/src/model/executor.rs b/runtime/rust/prompty/src/model/executor.rs new file mode 100644 index 00000000..597d0dc4 --- /dev/null +++ b/runtime/rust/prompty/src/model/executor.rs @@ -0,0 +1,25 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + + +use super::message::Message; + +use super::prompty::Prompty; + +use super::tool_call::ToolCall; + +/// Calls an LLM provider with messages and returns the raw provider response. +#[async_trait::async_trait] +pub trait Executor: Send + Sync { + /// Call an LLM provider with messages and return the raw response + async fn execute(&self, agent: &Prompty, messages: &Vec) -> Result>; + /// Call an LLM provider and return a streaming response. Returns a language-specific async iterable/stream of raw chunks. Not all providers support streaming; the default implementation should signal lack of support. + async fn execute_stream(&self, agent: &Prompty, messages: &Vec) -> Result> { + Err("not supported".into()) + } + /// Format tool call results into messages for the next iteration + fn format_tool_messages(&self, raw_response: &serde_json::Value, tool_calls: &Vec, tool_results: &Vec, text_content: &Option) -> Vec; +} diff --git a/runtime/rust/prompty/src/model/format_config.rs b/runtime/rust/prompty/src/model/format_config.rs index 1c5e2c16..237cc4ff 100644 --- a/runtime/rust/prompty/src/model/format_config.rs +++ b/runtime/rust/prompty/src/model/format_config.rs @@ -1,5 +1,9 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; /// Template format definition @@ -38,20 +42,12 @@ impl FormatConfig { let value = ctx.process_input(value.clone()); if let Some(s) = value.as_str() { let value = s.to_string(); - let expansion = serde_json::json!({"kind":value}); - return Self::load_from_value(&expansion, ctx); + return FormatConfig { kind: value.into(), ..Default::default() }; } Self { - kind: value - .get("kind") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), + kind: value.get("kind").and_then(|v| v.as_str()).unwrap_or_default().to_string(), strict: value.get("strict").and_then(|v| v.as_bool()), - options: value - .get("options") - .cloned() - .unwrap_or(serde_json::Value::Null), + options: value.get("options").cloned().unwrap_or(serde_json::Value::Null), } } @@ -60,11 +56,9 @@ impl FormatConfig { /// Calls `ctx.process_dict` after serialization. pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); + // Write base fields if !self.kind.is_empty() { - result.insert( - "kind".to_string(), - serde_json::Value::String(self.kind.clone()), - ); + result.insert("kind".to_string(), serde_json::Value::String(self.kind.clone())); } if let Some(val) = self.strict { result.insert("strict".to_string(), serde_json::Value::Bool(val)); @@ -89,4 +83,5 @@ impl FormatConfig { pub fn as_options_dict(&self) -> Option<&serde_json::Map> { self.options.as_object() } + } diff --git a/runtime/rust/prompty/src/model/guardrail_result.rs b/runtime/rust/prompty/src/model/guardrail_result.rs new file mode 100644 index 00000000..cefc2ce6 --- /dev/null +++ b/runtime/rust/prompty/src/model/guardrail_result.rs @@ -0,0 +1,87 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +/// The result of a guardrail evaluation. Guardrails are safety checks that run at specific phases of the agent loop and can allow, deny, or rewrite content. +#[derive(Debug, Clone, Default)] +pub struct GuardrailResult { + /// Whether the content passed the guardrail check + pub allowed: bool, + /// Explanation of why the content was allowed or denied + pub reason: Option, + /// Optional rewritten content to replace the original + pub rewrite: Option, +} + +impl GuardrailResult { + /// Create a new GuardrailResult with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load GuardrailResult from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load GuardrailResult from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load GuardrailResult from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + allowed: value.get("allowed").and_then(|v| v.as_bool()).unwrap_or(false), + reason: value.get("reason").and_then(|v| v.as_str()).map(|s| s.to_string()), + rewrite: value.get("rewrite").cloned(), + } + } + + /// Serialize GuardrailResult to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + result.insert("allowed".to_string(), serde_json::Value::Bool(self.allowed)); + if let Some(ref val) = self.reason { + result.insert("reason".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(ref val) = self.rewrite { + result.insert("rewrite".to_string(), val.clone()); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize GuardrailResult to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize GuardrailResult to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } + /// Create a GuardrailResult with preset field values. + pub fn rewrite(rewrite: impl Into) -> Self { + GuardrailResult { allowed: true, rewrite: Some(rewrite.into()), ..Default::default() } + } + /// Create a GuardrailResult with preset field values. + pub fn deny(reason: impl Into) -> Self { + GuardrailResult { allowed: false, reason: Some(reason.into()), ..Default::default() } + } + /// Create a GuardrailResult with preset field values. + pub fn allow() -> Self { + GuardrailResult { allowed: true, ..Default::default() } + } +} diff --git a/runtime/rust/prompty/src/model/mcp_approval_mode.rs b/runtime/rust/prompty/src/model/mcp_approval_mode.rs index e406e8c9..4138b1db 100644 --- a/runtime/rust/prompty/src/model/mcp_approval_mode.rs +++ b/runtime/rust/prompty/src/model/mcp_approval_mode.rs @@ -1,5 +1,9 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; /// The approval mode for MCP server tools. When kind is "specify", use alwaysRequireApprovalTools and neverRequireApprovalTools to control per-tool approval. For "always" and "never", those fields are ignored. @@ -38,31 +42,12 @@ impl McpApprovalMode { let value = ctx.process_input(value.clone()); if let Some(s) = value.as_str() { let value = s.to_string(); - let expansion = serde_json::json!({"kind":value}); - return Self::load_from_value(&expansion, ctx); + return McpApprovalMode { kind: value.into(), ..Default::default() }; } Self { - kind: value - .get("kind") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - always_require_approval_tools: value - .get("alwaysRequireApprovalTools") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect() - }), - never_require_approval_tools: value - .get("neverRequireApprovalTools") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect() - }), + kind: value.get("kind").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + always_require_approval_tools: value.get("alwaysRequireApprovalTools").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()), + never_require_approval_tools: value.get("neverRequireApprovalTools").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()), } } @@ -71,23 +56,15 @@ impl McpApprovalMode { /// Calls `ctx.process_dict` after serialization. pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); + // Write base fields if !self.kind.is_empty() { - result.insert( - "kind".to_string(), - serde_json::Value::String(self.kind.clone()), - ); + result.insert("kind".to_string(), serde_json::Value::String(self.kind.clone())); } if let Some(ref items) = self.always_require_approval_tools { - result.insert( - "alwaysRequireApprovalTools".to_string(), - serde_json::to_value(items).unwrap_or(serde_json::Value::Null), - ); + result.insert("alwaysRequireApprovalTools".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null)); } if let Some(ref items) = self.never_require_approval_tools { - result.insert( - "neverRequireApprovalTools".to_string(), - serde_json::to_value(items).unwrap_or(serde_json::Value::Null), - ); + result.insert("neverRequireApprovalTools".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null)); } ctx.process_dict(serde_json::Value::Object(result)) } diff --git a/runtime/rust/prompty/src/model/message.rs b/runtime/rust/prompty/src/model/message.rs new file mode 100644 index 00000000..8b12b085 --- /dev/null +++ b/runtime/rust/prompty/src/model/message.rs @@ -0,0 +1,124 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +use super::content_part::{ContentPart, ContentPartKind}; + +/// A message in a conversation. Messages have a role and a list of content parts representing the different modalities of the message content. +#[derive(Debug, Clone, Default)] +pub struct Message { + /// The role of the message sender + pub role: String, + /// The content parts of the message + pub parts: Vec, + /// Optional metadata associated with the message + pub metadata: serde_json::Value, +} + +impl Message { + /// Create a new Message with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load Message from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load Message from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load Message from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + role: value.get("role").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + parts: value.get("parts").map(|v| Self::load_parts(v, ctx)).unwrap_or_default(), + metadata: value.get("metadata").cloned().unwrap_or(serde_json::Value::Null), + } + } + + /// Serialize Message to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if !self.role.is_empty() { + result.insert("role".to_string(), serde_json::Value::String(self.role.clone())); + } + if !self.parts.is_empty() { + result.insert("parts".to_string(), Self::save_parts(&self.parts, ctx)); + } + if !self.metadata.is_null() { + result.insert("metadata".to_string(), self.metadata.clone()); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize Message to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize Message to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } + /// Returns typed reference to the map if the field is an object. + /// Returns `None` if the field is null or not an object. + pub fn as_metadata_dict(&self) -> Option<&serde_json::Map> { + self.metadata.as_object() + } + + + /// Load a collection of ContentPart from a JSON value. + /// Handles both array format `[{...}]`. + fn load_parts(data: &serde_json::Value, ctx: &LoadContext) -> Vec { + match data { + serde_json::Value::Array(arr) => { + arr.iter().map(|v| ContentPart::load_from_value(v, ctx)).collect() + } + + _ => Vec::new(), + + } + } + + /// Save a collection of ContentPart to a JSON value. + fn save_parts(items: &[ContentPart], ctx: &SaveContext) -> serde_json::Value { + + serde_json::Value::Array(items.iter().map(|item| item.to_value(ctx)).collect::>()) + + } + /// Create a Message with preset field values. + pub fn assistant(text: impl Into) -> Self { + Message { role: "assistant".to_string(), parts: vec![ContentPart { kind: ContentPartKind::TextPart { value: text.into() }, ..Default::default() }], ..Default::default() } + } + /// Create a Message with preset field values. + pub fn system(text: impl Into) -> Self { + Message { role: "system".to_string(), parts: vec![ContentPart { kind: ContentPartKind::TextPart { value: text.into() }, ..Default::default() }], ..Default::default() } + } + /// Create a Message with preset field values. + pub fn user(text: impl Into) -> Self { + Message { role: "user".to_string(), parts: vec![ContentPart { kind: ContentPartKind::TextPart { value: text.into() }, ..Default::default() }], ..Default::default() } + } +} +/// Helpers for [`Message`]. Implement in a separate file. +pub trait MessageHelpers { + /// Return plain string if all parts are text, else a list of content part dicts for wire serialization + fn to_text_content(&self) -> serde_json::Value; + /// Concatenate all TextPart values joined by newline + fn text(&self) -> String; +} diff --git a/runtime/rust/prompty/src/model/messages_updated_payload.rs b/runtime/rust/prompty/src/model/messages_updated_payload.rs new file mode 100644 index 00000000..3d926370 --- /dev/null +++ b/runtime/rust/prompty/src/model/messages_updated_payload.rs @@ -0,0 +1,87 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +use super::message::Message; + +/// Payload for "messages_updated" events — the conversation state has changed. +#[derive(Debug, Clone, Default)] +pub struct MessagesUpdatedPayload { + /// The current full message list after the update + pub messages: Vec, +} + +impl MessagesUpdatedPayload { + /// Create a new MessagesUpdatedPayload with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load MessagesUpdatedPayload from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load MessagesUpdatedPayload from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load MessagesUpdatedPayload from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + messages: value.get("messages").map(|v| Self::load_messages(v, ctx)).unwrap_or_default(), + } + } + + /// Serialize MessagesUpdatedPayload to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if !self.messages.is_empty() { + result.insert("messages".to_string(), Self::save_messages(&self.messages, ctx)); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize MessagesUpdatedPayload to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize MessagesUpdatedPayload to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } + + /// Load a collection of Message from a JSON value. + /// Handles both array format `[{...}]`. + fn load_messages(data: &serde_json::Value, ctx: &LoadContext) -> Vec { + match data { + serde_json::Value::Array(arr) => { + arr.iter().map(|v| Message::load_from_value(v, ctx)).collect() + } + + _ => Vec::new(), + + } + } + + /// Save a collection of Message to a JSON value. + fn save_messages(items: &[Message], ctx: &SaveContext) -> serde_json::Value { + + serde_json::Value::Array(items.iter().map(|item| item.to_value(ctx)).collect::>()) + + } +} diff --git a/runtime/rust/prompty/src/model/mod.rs b/runtime/rust/prompty/src/model/mod.rs index ba9059be..4e30c0dc 100644 --- a/runtime/rust/prompty/src/model/mod.rs +++ b/runtime/rust/prompty/src/model/mod.rs @@ -12,7 +12,6 @@ pub use connection::*; pub mod model_options; pub use model_options::*; -#[allow(clippy::module_inception)] pub mod model; pub use model::*; @@ -36,3 +35,84 @@ pub use template::*; pub mod prompty; pub use prompty::*; + +pub mod content_part; +pub use content_part::*; + +pub mod message; +pub use message::*; + +pub mod tool_context; +pub use tool_context::*; + +pub mod tool_result; +pub use tool_result::*; + +pub mod tool_dispatch_result; +pub use tool_dispatch_result::*; + +pub mod tool_call; +pub use tool_call::*; + +pub mod guardrail_result; +pub use guardrail_result::*; + +pub mod thread_marker; +pub use thread_marker::*; + +pub mod token_usage; +pub use token_usage::*; + +pub mod model_info; +pub use model_info::*; + +pub mod compaction_config; +pub use compaction_config::*; + +pub mod turn_options; +pub use turn_options::*; + +pub mod renderer; +pub use renderer::*; + +pub mod parser; +pub use parser::*; + +pub mod executor; +pub use executor::*; + +pub mod processor; +pub use processor::*; + +pub mod token_event_payload; +pub use token_event_payload::*; + +pub mod thinking_event_payload; +pub use thinking_event_payload::*; + +pub mod tool_call_start_payload; +pub use tool_call_start_payload::*; + +pub mod tool_result_payload; +pub use tool_result_payload::*; + +pub mod status_event_payload; +pub use status_event_payload::*; + +pub mod messages_updated_payload; +pub use messages_updated_payload::*; + +pub mod done_event_payload; +pub use done_event_payload::*; + +pub mod error_event_payload; +pub use error_event_payload::*; + +pub mod compaction_complete_payload; +pub use compaction_complete_payload::*; + +pub mod compaction_failed_payload; +pub use compaction_failed_payload::*; + +pub mod stream_chunk; +pub use stream_chunk::*; diff --git a/runtime/rust/prompty/src/model/model.rs b/runtime/rust/prompty/src/model/model.rs index 11534bb5..0382c91f 100644 --- a/runtime/rust/prompty/src/model/model.rs +++ b/runtime/rust/prompty/src/model/model.rs @@ -1,7 +1,13 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; +use super::connection::Connection; + use super::model_options::ModelOptions; /// Model for defining the structure and behavior of AI agents. This model includes properties for specifying the model's provider, connection details, and various options. It allows for flexible configuration of AI models to suit different use cases and requirements. @@ -44,31 +50,14 @@ impl Model { let value = ctx.process_input(value.clone()); if let Some(s) = value.as_str() { let value = s.to_string(); - let expansion = serde_json::json!({"id":value}); - return Self::load_from_value(&expansion, ctx); + return Model { id: value.into(), ..Default::default() }; } Self { - id: value - .get("id") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - provider: value - .get("provider") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - api_type: value - .get("apiType") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - connection: value - .get("connection") - .cloned() - .unwrap_or(serde_json::Value::Null), - options: value - .get("options") - .filter(|v| v.is_object() || v.is_array() || v.is_string()) - .map(|v| ModelOptions::load_from_value(v, ctx)), + id: value.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + provider: value.get("provider").and_then(|v| v.as_str()).map(|s| s.to_string()), + api_type: value.get("apiType").and_then(|v| v.as_str()).map(|s| s.to_string()), + connection: value.get("connection").cloned().unwrap_or(serde_json::Value::Null), + options: value.get("options").filter(|v| v.is_object() || v.is_array() || v.is_string()).map(|v| ModelOptions::load_from_value(v, ctx)), } } @@ -77,20 +66,15 @@ impl Model { /// Calls `ctx.process_dict` after serialization. pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); + // Write base fields if !self.id.is_empty() { result.insert("id".to_string(), serde_json::Value::String(self.id.clone())); } if let Some(ref val) = self.provider { - result.insert( - "provider".to_string(), - serde_json::Value::String(val.clone()), - ); + result.insert("provider".to_string(), serde_json::Value::String(val.clone())); } if let Some(ref val) = self.api_type { - result.insert( - "apiType".to_string(), - serde_json::Value::String(val.clone()), - ); + result.insert("apiType".to_string(), serde_json::Value::String(val.clone())); } if !self.connection.is_null() { result.insert("connection".to_string(), self.connection.clone()); diff --git a/runtime/rust/prompty/src/model/model_info.rs b/runtime/rust/prompty/src/model/model_info.rs new file mode 100644 index 00000000..49e90b92 --- /dev/null +++ b/runtime/rust/prompty/src/model/model_info.rs @@ -0,0 +1,107 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + +use super::context::{LoadContext, SaveContext}; + +/// Information about a model available from a provider. Used by provider-level model discovery to report which models are available and their capabilities. Not all providers return all fields — implementations SHOULD populate as many fields as the provider's API supports and MAY enrich sparse results from a built-in lookup table of known models. +#[derive(Debug, Clone, Default)] +pub struct ModelInfo { + /// The model identifier (e.g., 'gpt-4o', 'claude-3-opus') + pub id: String, + /// Human-readable display name + pub display_name: Option, + /// The organization or entity that owns the model + pub owned_by: Option, + /// Maximum context window size in tokens + pub context_window: Option, + /// Input modalities the model accepts (e.g., 'text', 'image', 'audio') + pub input_modalities: Option>, + /// Output modalities the model can produce (e.g., 'text', 'audio') + pub output_modalities: Option>, + /// Additional provider-specific properties + pub additional_properties: serde_json::Value, +} + +impl ModelInfo { + /// Create a new ModelInfo with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load ModelInfo from a JSON string. + pub fn from_json(json: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load ModelInfo from a YAML string. + pub fn from_yaml(yaml: &str, ctx: &LoadContext) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value, ctx)) + } + + /// Load ModelInfo from a `serde_json::Value`. + /// + /// Calls `ctx.process_input` before field extraction. + pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { + let value = ctx.process_input(value.clone()); + Self { + id: value.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + display_name: value.get("displayName").and_then(|v| v.as_str()).map(|s| s.to_string()), + owned_by: value.get("ownedBy").and_then(|v| v.as_str()).map(|s| s.to_string()), + context_window: value.get("contextWindow").and_then(|v| v.as_i64()).map(|v| v as i32), + input_modalities: value.get("inputModalities").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()), + output_modalities: value.get("outputModalities").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()), + additional_properties: value.get("additionalProperties").cloned().unwrap_or(serde_json::Value::Null), + } + } + + /// Serialize ModelInfo to a `serde_json::Value`. + /// + /// Calls `ctx.process_dict` after serialization. + pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { + let mut result = serde_json::Map::new(); + // Write base fields + if !self.id.is_empty() { + result.insert("id".to_string(), serde_json::Value::String(self.id.clone())); + } + if let Some(ref val) = self.display_name { + result.insert("displayName".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(ref val) = self.owned_by { + result.insert("ownedBy".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(val) = self.context_window { + result.insert("contextWindow".to_string(), serde_json::Value::Number(serde_json::Number::from(val))); + } + if let Some(ref items) = self.input_modalities { + result.insert("inputModalities".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null)); + } + if let Some(ref items) = self.output_modalities { + result.insert("outputModalities".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null)); + } + if !self.additional_properties.is_null() { + result.insert("additionalProperties".to_string(), self.additional_properties.clone()); + } + ctx.process_dict(serde_json::Value::Object(result)) + } + + /// Serialize ModelInfo to a JSON string. + pub fn to_json(&self, ctx: &SaveContext) -> Result { + serde_json::to_string_pretty(&self.to_value(ctx)) + } + + /// Serialize ModelInfo to a YAML string. + pub fn to_yaml(&self, ctx: &SaveContext) -> Result { + serde_yaml::to_string(&self.to_value(ctx)) + } + /// Returns typed reference to the map if the field is an object. + /// Returns `None` if the field is null or not an object. + pub fn as_additional_properties_dict(&self) -> Option<&serde_json::Map> { + self.additional_properties.as_object() + } + +} diff --git a/runtime/rust/prompty/src/model/model_options.rs b/runtime/rust/prompty/src/model/model_options.rs index ae7b44e1..b2ba6ff8 100644 --- a/runtime/rust/prompty/src/model/model_options.rs +++ b/runtime/rust/prompty/src/model/model_options.rs @@ -1,9 +1,14 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; /// Options for configuring the behavior of the AI model. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] pub struct ModelOptions { /// The frequency penalty to apply to the model's output pub frequency_penalty: Option, @@ -51,40 +56,16 @@ impl ModelOptions { pub fn load_from_value(value: &serde_json::Value, ctx: &LoadContext) -> Self { let value = ctx.process_input(value.clone()); Self { - frequency_penalty: value - .get("frequencyPenalty") - .and_then(|v| v.as_f64()) - .map(|n| n as f32), - max_output_tokens: value - .get("maxOutputTokens") - .and_then(|v| v.as_i64()) - .map(|n| n as i32), - presence_penalty: value - .get("presencePenalty") - .and_then(|v| v.as_f64()) - .map(|n| n as f32), - seed: value.get("seed").and_then(|v| v.as_i64()).map(|n| n as i32), - temperature: value - .get("temperature") - .and_then(|v| v.as_f64()) - .map(|n| n as f32), - top_k: value.get("topK").and_then(|v| v.as_i64()).map(|n| n as i32), - top_p: value.get("topP").and_then(|v| v.as_f64()).map(|n| n as f32), - stop_sequences: value - .get("stopSequences") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect() - }), - allow_multiple_tool_calls: value - .get("allowMultipleToolCalls") - .and_then(|v| v.as_bool()), - additional_properties: value - .get("additionalProperties") - .cloned() - .unwrap_or(serde_json::Value::Null), + frequency_penalty: value.get("frequencyPenalty").and_then(|v| v.as_f64()).map(|v| v as f32), + max_output_tokens: value.get("maxOutputTokens").and_then(|v| v.as_i64()).map(|v| v as i32), + presence_penalty: value.get("presencePenalty").and_then(|v| v.as_f64()).map(|v| v as f32), + seed: value.get("seed").and_then(|v| v.as_i64()).map(|v| v as i32), + temperature: value.get("temperature").and_then(|v| v.as_f64()).map(|v| v as f32), + top_k: value.get("topK").and_then(|v| v.as_i64()).map(|v| v as i32), + top_p: value.get("topP").and_then(|v| v.as_f64()).map(|v| v as f32), + stop_sequences: value.get("stopSequences").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()), + allow_multiple_tool_calls: value.get("allowMultipleToolCalls").and_then(|v| v.as_bool()), + additional_properties: value.get("additionalProperties").cloned().unwrap_or(serde_json::Value::Null), } } @@ -93,65 +74,36 @@ impl ModelOptions { /// Calls `ctx.process_dict` after serialization. pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); - if let Some(ref val) = self.frequency_penalty { - result.insert( - "frequencyPenalty".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + // Write base fields + if let Some(val) = self.frequency_penalty { + result.insert("frequencyPenalty".to_string(), serde_json::Number::from_f64(val as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)); } - if let Some(ref val) = self.max_output_tokens { - result.insert( - "maxOutputTokens".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + if let Some(val) = self.max_output_tokens { + result.insert("maxOutputTokens".to_string(), serde_json::Value::Number(serde_json::Number::from(val))); } - if let Some(ref val) = self.presence_penalty { - result.insert( - "presencePenalty".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + if let Some(val) = self.presence_penalty { + result.insert("presencePenalty".to_string(), serde_json::Number::from_f64(val as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)); } - if let Some(ref val) = self.seed { - result.insert( - "seed".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + if let Some(val) = self.seed { + result.insert("seed".to_string(), serde_json::Value::Number(serde_json::Number::from(val))); } - if let Some(ref val) = self.temperature { - result.insert( - "temperature".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + if let Some(val) = self.temperature { + result.insert("temperature".to_string(), serde_json::Number::from_f64(val as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)); } - if let Some(ref val) = self.top_k { - result.insert( - "topK".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + if let Some(val) = self.top_k { + result.insert("topK".to_string(), serde_json::Value::Number(serde_json::Number::from(val))); } - if let Some(ref val) = self.top_p { - result.insert( - "topP".to_string(), - serde_json::to_value(val).unwrap_or(serde_json::Value::Null), - ); + if let Some(val) = self.top_p { + result.insert("topP".to_string(), serde_json::Number::from_f64(val as f64).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)); } if let Some(ref items) = self.stop_sequences { - result.insert( - "stopSequences".to_string(), - serde_json::to_value(items).unwrap_or(serde_json::Value::Null), - ); + result.insert("stopSequences".to_string(), serde_json::to_value(items).unwrap_or(serde_json::Value::Null)); } if let Some(val) = self.allow_multiple_tool_calls { - result.insert( - "allowMultipleToolCalls".to_string(), - serde_json::Value::Bool(val), - ); + result.insert("allowMultipleToolCalls".to_string(), serde_json::Value::Bool(val)); } if !self.additional_properties.is_null() { - result.insert( - "additionalProperties".to_string(), - self.additional_properties.clone(), - ); + result.insert("additionalProperties".to_string(), self.additional_properties.clone()); } ctx.process_dict(serde_json::Value::Object(result)) } @@ -165,11 +117,37 @@ impl ModelOptions { pub fn to_yaml(&self, ctx: &SaveContext) -> Result { serde_yaml::to_string(&self.to_value(ctx)) } + + /// Convert to provider-specific wire format. + pub fn to_wire(&self, provider: &str) -> serde_json::Value { + let data = serde_json::to_value(self).unwrap_or_default(); + let mut result = serde_json::Map::new(); + let wire_map: std::collections::HashMap<&str, std::collections::HashMap<&str, &str>> = std::collections::HashMap::from([ + ("frequencyPenalty", std::collections::HashMap::from([("openai", "frequency_penalty")])), + ("maxOutputTokens", std::collections::HashMap::from([("openai", "max_completion_tokens"), ("responses", "max_output_tokens"), ("anthropic", "max_tokens")])), + ("presencePenalty", std::collections::HashMap::from([("openai", "presence_penalty")])), + ("seed", std::collections::HashMap::from([("openai", "seed")])), + ("temperature", std::collections::HashMap::from([("openai", "temperature"), ("responses", "temperature"), ("anthropic", "temperature")])), + ("topK", std::collections::HashMap::from([("openai", "top_k"), ("anthropic", "top_k")])), + ("topP", std::collections::HashMap::from([("openai", "top_p"), ("responses", "top_p"), ("anthropic", "top_p")])), + ("stopSequences", std::collections::HashMap::from([("openai", "stop"), ("anthropic", "stop_sequences")])), + ("allowMultipleToolCalls", std::collections::HashMap::from([("openai", "parallel_tool_calls")])), + ]); + if let serde_json::Value::Object(map) = data { + for (key, value) in map { + if let Some(mapping) = wire_map.get(key.as_str()) { + if let Some(wire_name) = mapping.get(provider) { + result.insert(wire_name.to_string(), value); + } + } + } + } + serde_json::Value::Object(result) + } /// Returns typed reference to the map if the field is an object. /// Returns `None` if the field is null or not an object. - pub fn as_additional_properties_dict( - &self, - ) -> Option<&serde_json::Map> { + pub fn as_additional_properties_dict(&self) -> Option<&serde_json::Map> { self.additional_properties.as_object() } + } diff --git a/runtime/rust/prompty/src/model/parser.rs b/runtime/rust/prompty/src/model/parser.rs new file mode 100644 index 00000000..e4dd67f7 --- /dev/null +++ b/runtime/rust/prompty/src/model/parser.rs @@ -0,0 +1,21 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + + +use super::message::Message; + +use super::prompty::Prompty; + +/// Parses rendered prompt text into an array of structured messages with role markers. +#[async_trait::async_trait] +pub trait Parser: Send + Sync { + /// Pre-process a template before rendering, returning modified template and context + fn pre_render(&self, template: &String) -> Option { + None + } + /// Parse rendered text into a structured message array + async fn parse(&self, agent: &Prompty, rendered: &String, context: &Option) -> Result, Box>; +} diff --git a/runtime/rust/prompty/src/model/parser_config.rs b/runtime/rust/prompty/src/model/parser_config.rs index 6a864640..b234a590 100644 --- a/runtime/rust/prompty/src/model/parser_config.rs +++ b/runtime/rust/prompty/src/model/parser_config.rs @@ -1,5 +1,9 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; /// Template parser definition @@ -36,19 +40,11 @@ impl ParserConfig { let value = ctx.process_input(value.clone()); if let Some(s) = value.as_str() { let value = s.to_string(); - let expansion = serde_json::json!({"kind":value}); - return Self::load_from_value(&expansion, ctx); + return ParserConfig { kind: value.into(), ..Default::default() }; } Self { - kind: value - .get("kind") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - options: value - .get("options") - .cloned() - .unwrap_or(serde_json::Value::Null), + kind: value.get("kind").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + options: value.get("options").cloned().unwrap_or(serde_json::Value::Null), } } @@ -57,11 +53,9 @@ impl ParserConfig { /// Calls `ctx.process_dict` after serialization. pub fn to_value(&self, ctx: &SaveContext) -> serde_json::Value { let mut result = serde_json::Map::new(); + // Write base fields if !self.kind.is_empty() { - result.insert( - "kind".to_string(), - serde_json::Value::String(self.kind.clone()), - ); + result.insert("kind".to_string(), serde_json::Value::String(self.kind.clone())); } if !self.options.is_null() { result.insert("options".to_string(), self.options.clone()); @@ -83,4 +77,5 @@ impl ParserConfig { pub fn as_options_dict(&self) -> Option<&serde_json::Map> { self.options.as_object() } + } diff --git a/runtime/rust/prompty/src/model/processor.rs b/runtime/rust/prompty/src/model/processor.rs new file mode 100644 index 00000000..9e72006c --- /dev/null +++ b/runtime/rust/prompty/src/model/processor.rs @@ -0,0 +1,19 @@ + + +// Code generated by AgentSchema emitter; DO NOT EDIT. + +#![allow(unused_imports, dead_code)] + + +use super::prompty::Prompty; + +/// Extracts a clean, typed result from a raw LLM provider response. +#[async_trait::async_trait] +pub trait Processor: Send + Sync { + /// Extract a clean result from a raw LLM response + async fn process(&self, agent: &Prompty, response: &serde_json::Value) -> Result>; + /// Process a streaming response into a stream of StreamChunk items. Takes raw chunks from the executor and yields processed text, thinking, tool, or error chunks. Not all providers support streaming; the default implementation should signal lack of support. + async fn process_stream(&self, stream: &serde_json::Value) -> Result> { + Err("not supported".into()) + } +} diff --git a/runtime/rust/prompty/src/model/prompty.rs b/runtime/rust/prompty/src/model/prompty.rs index bf47f178..2597b83a 100644 --- a/runtime/rust/prompty/src/model/prompty.rs +++ b/runtime/rust/prompty/src/model/prompty.rs @@ -1,5 +1,9 @@ + + // Code generated by AgentSchema emitter; DO NOT EDIT. +#![allow(unused_imports, dead_code)] + use super::context::{LoadContext, SaveContext}; use super::model::Model; @@ -10,7 +14,7 @@ use super::template::Template; use super::tool::Tool; -/// A Prompty is a markdown file format for LLM prompts. The frontmatter defines structured metadata including model configuration, input/output schemas, tools, and template settings. The markdown body becomes the instructions. This is the single root type for the Prompty schema — there is no abstract base class or kind discriminator. A .prompty file always produces a Prompty instance. +/// A Prompty is a markdown file format for LLM prompts. The frontmatter defines structured metadata including model configuration, input/output schemas, tools, and template settings. The markdown body becomes the instructions. This is the single root type for the Prompty schema — there is no abstract base class or kind discriminator. A .prompty file always produces a Prompty instance. #[derive(Debug, Clone, Default)] pub struct Prompty { /// Human-readable name of the prompt @@ -22,13 +26,13 @@ pub struct Prompty { /// Additional metadata including authors, tags, and other arbitrary properties pub metadata: serde_json::Value, /// Input parameters that participate in template rendering - pub inputs: serde_json::Value, + pub inputs: Vec, /// Expected output format and structure - pub outputs: serde_json::Value, + pub outputs: Vec, /// AI model configuration pub model: Model, /// Tools available for extended functionality - pub tools: serde_json::Value, + pub tools: Vec, /// Template configuration for prompt rendering pub template: Option