Skip to content

feat: Add prompt parameter support for stored prompts in Responses API (Fixes #500) #501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions examples/Responses/Example03_StoredPrompts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using NUnit.Framework;
using OpenAI.Responses;
using System;
using System.Collections.Generic;

namespace OpenAI.Examples;

public partial class ResponseExamples
{
[Test]
public void Example03_StoredPrompts()
{
OpenAIResponseClient client = new(model: "gpt-4o", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

// Create options using a stored prompt
ResponseCreationOptions options = new()
{
Prompt = new ResponsePrompt
{
Id = "your-stored-prompt-id",
Version = "v1.0"
}
};

// Add variables to substitute in the prompt template
options.Prompt.Variables["location"] = "San Francisco";
options.Prompt.Variables["unit"] = "celsius";

// Use stored prompt with variables
IEnumerable<ResponseItem> inputItems = Array.Empty<ResponseItem>();
OpenAIResponse response = client.CreateResponse(inputItems, options);

Console.WriteLine($"[ASSISTANT]: {response.GetOutputText()}");
}
}
36 changes: 36 additions & 0 deletions examples/Responses/Example03_StoredPromptsAsync.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using NUnit.Framework;
using OpenAI.Responses;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace OpenAI.Examples;

public partial class ResponseExamples
{
[Test]
public async Task Example03_StoredPromptsAsync()
{
OpenAIResponseClient client = new(model: "gpt-4o", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

// Create options using a stored prompt
ResponseCreationOptions options = new()
{
Prompt = new ResponsePrompt
{
Id = "your-stored-prompt-id",
Version = "v1.0"
}
};

// Add variables to substitute in the prompt template
options.Prompt.Variables["location"] = "San Francisco";
options.Prompt.Variables["unit"] = "celsius";

// Use stored prompt with variables
IEnumerable<ResponseItem> inputItems = Array.Empty<ResponseItem>();
OpenAIResponse response = await client.CreateResponseAsync(inputItems, options);

Console.WriteLine($"[ASSISTANT]: {response.GetOutputText()}");
}
}
13 changes: 13 additions & 0 deletions src/Custom/Responses/ResponseCreationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ public partial class ResponseCreationOptions
// CUSTOM: Made internal. This value comes from a parameter on the client method.
internal bool? Stream { get; set; }

// CUSTOM: Added prompt parameter support for stored prompts.
[CodeGenMember("Prompt")]
public ResponsePrompt Prompt { get; set; }

// CUSTOM: Added public default constructor now that there are no required properties.
public ResponseCreationOptions()
{
Input = new ChangeTrackingList<ResponseItem>();
Metadata = new ChangeTrackingDictionary<string, string>();
Tools = new ChangeTrackingList<ResponseTool>();
Include = new ChangeTrackingList<InternalCreateResponsesRequestIncludable>();
}

// CUSTOM: Renamed.
[CodeGenMember("User")]
public string EndUserId { get; set; }
Expand Down
154 changes: 154 additions & 0 deletions src/Custom/Responses/ResponsePrompt.Serialization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Text.Json;

namespace OpenAI.Responses;

public partial class ResponsePrompt : IJsonModel<ResponsePrompt>
{
void IJsonModel<ResponsePrompt>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
=> CustomSerializationHelpers.SerializeInstance(this, SerializeResponsePrompt, writer, options);

ResponsePrompt IJsonModel<ResponsePrompt>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
=> CustomSerializationHelpers.DeserializeNewInstance(this, DeserializeResponsePrompt, ref reader, options);

BinaryData IPersistableModel<ResponsePrompt>.Write(ModelReaderWriterOptions options)
=> CustomSerializationHelpers.SerializeInstance(this, options);

ResponsePrompt IPersistableModel<ResponsePrompt>.Create(BinaryData data, ModelReaderWriterOptions options)
=> CustomSerializationHelpers.DeserializeNewInstance(this, DeserializeResponsePrompt, data, options);

string IPersistableModel<ResponsePrompt>.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";

internal static void SerializeResponsePrompt(ResponsePrompt instance, Utf8JsonWriter writer, ModelReaderWriterOptions options)
{
writer.WriteStartObject();

if (instance.Id != null)
{
writer.WritePropertyName("id");
writer.WriteStringValue(instance.Id);
}

if (instance.Version != null)
{
writer.WritePropertyName("version");
writer.WriteStringValue(instance.Version);
}

if (instance.Variables != null && instance.Variables.Count > 0)
{
writer.WritePropertyName("variables");
writer.WriteStartObject();
foreach (var variable in instance.Variables)
{
writer.WritePropertyName(variable.Key);
if (variable.Value is JsonElement element)
{
#if NET6_0_OR_GREATER
writer.WriteRawValue(element.GetRawText());
#else
using JsonDocument document = JsonDocument.Parse(element.GetRawText());
JsonSerializer.Serialize(writer, document.RootElement);
#endif
}
else if (variable.Value != null)
{
// Handle primitive types directly
switch (variable.Value)
{
case string str:
writer.WriteStringValue(str);
break;
case int intVal:
writer.WriteNumberValue(intVal);
break;
case long longVal:
writer.WriteNumberValue(longVal);
break;
case float floatVal:
writer.WriteNumberValue(floatVal);
break;
case double doubleVal:
writer.WriteNumberValue(doubleVal);
break;
case decimal decimalVal:
writer.WriteNumberValue(decimalVal);
break;
case bool boolVal:
writer.WriteBooleanValue(boolVal);
break;
default:
// For other types, write as string value
writer.WriteStringValue(variable.Value.ToString());
break;
}
}
else
{
writer.WriteNullValue();
}
}
writer.WriteEndObject();
}

writer.WriteEndObject();
}

internal static ResponsePrompt DeserializeResponsePrompt(JsonElement element, ModelReaderWriterOptions options = null)
{
if (element.ValueKind == JsonValueKind.Null)
{
return null;
}

string id = null;
string version = null;
Dictionary<string, object> variables = new Dictionary<string, object>();

foreach (var property in element.EnumerateObject())
{
if (property.NameEquals("id"))
{
id = property.Value.GetString();
}
else if (property.NameEquals("version"))
{
version = property.Value.GetString();
}
else if (property.NameEquals("variables"))
{
foreach (var variable in property.Value.EnumerateObject())
{
// Handle different JSON value types appropriately
object value = variable.Value.ValueKind switch
{
JsonValueKind.String => variable.Value.GetString(),
JsonValueKind.Number => variable.Value.TryGetInt32(out int intVal) ? intVal : variable.Value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => variable.Value.GetRawText() // For objects/arrays, store as raw JSON string
};
variables[variable.Name] = value;
}
}
}

var result = new ResponsePrompt();
result.Id = id;
result.Version = version;

// Add variables to the existing dictionary (Variables property is get-only)
if (variables.Count > 0)
{
foreach (var variable in variables)
{
result.Variables[variable.Key] = variable.Value;
}
}

return result;
}
}
21 changes: 21 additions & 0 deletions src/Custom/Responses/ResponsePrompt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;

namespace OpenAI.Responses;

[CodeGenType("ResponsesPrompt")]
public partial class ResponsePrompt
{
[CodeGenMember("Id")]
public string Id { get; set; }

[CodeGenMember("Version")]
public string Version { get; set; }

[CodeGenMember("Variables")]
public IDictionary<string, object> Variables { get; }

public ResponsePrompt()
{
Variables = new Dictionary<string, object>();
}
}
51 changes: 51 additions & 0 deletions tests/Responses/ResponsesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -905,4 +905,55 @@ public async Task CanCancelBackgroundResponses()
false);

private static OpenAIResponseClient GetTestClient(string overrideModel = null) => GetTestClient<OpenAIResponseClient>(TestScenario.Responses, overrideModel);

[Test]
public void ResponsePromptSerializationWorks()
{
ResponsePrompt prompt = new ResponsePrompt
{
Id = "test-prompt-id",
Version = "v1.0"
};
prompt.Variables["location"] = "San Francisco";
prompt.Variables["unit"] = "celsius";

string json = BinaryData.FromObjectAsJson(prompt).ToString();
JsonDocument document = JsonDocument.Parse(json);

Assert.IsTrue(document.RootElement.TryGetProperty("id", out JsonElement idElement));
Assert.AreEqual("test-prompt-id", idElement.GetString());

Assert.IsTrue(document.RootElement.TryGetProperty("version", out JsonElement versionElement));
Assert.AreEqual("v1.0", versionElement.GetString());

Assert.IsTrue(document.RootElement.TryGetProperty("variables", out JsonElement variablesElement));
Assert.IsTrue(variablesElement.TryGetProperty("location", out JsonElement locationElement));
Assert.AreEqual("San Francisco", locationElement.GetString());

Assert.IsTrue(variablesElement.TryGetProperty("unit", out JsonElement unitElement));
Assert.AreEqual("celsius", unitElement.GetString());
}

[Test]
public void ResponsePromptDeserializationWorks()
{
string json = """
{
"id": "test-prompt-id",
"version": "v1.0",
"variables": {
"location": "San Francisco",
"unit": "celsius"
}
}
""";

ResponsePrompt prompt = BinaryData.FromString(json).ToObjectFromJson<ResponsePrompt>();

Assert.AreEqual("test-prompt-id", prompt.Id);
Assert.AreEqual("v1.0", prompt.Version);
Assert.AreEqual(2, prompt.Variables.Count);
Assert.AreEqual("San Francisco", prompt.Variables["location"].ToString());
Assert.AreEqual("celsius", prompt.Variables["unit"].ToString());
}
}