Skip to content

Commit ff75da4

Browse files
authored
2.1.0-beta.1 staging: RealtimeConversationClient (#238)
* realtime client staging * add non-version-impacting .csproj update
1 parent c28661f commit ff75da4

File tree

251 files changed

+21795
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

251 files changed

+21795
-1
lines changed

api/OpenAI.netstandard2.0.cs

Lines changed: 703 additions & 0 deletions
Large diffs are not rendered by default.

src/Custom/OpenAIClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using OpenAI.Images;
99
using OpenAI.Models;
1010
using OpenAI.Moderations;
11+
using OpenAI.RealtimeConversation;
1112
using OpenAI.VectorStores;
1213
using System;
1314
using System.ClientModel;
@@ -42,6 +43,7 @@ namespace OpenAI;
4243
[CodeGenSuppress("_cachedLegacyCompletionClient")]
4344
[CodeGenSuppress("_cachedOpenAIModelClient")]
4445
[CodeGenSuppress("_cachedModerationClient")]
46+
[CodeGenSuppress("_cachedRealtimeConversationClient")]
4547
[CodeGenSuppress("_cachedVectorStoreClient")]
4648
[CodeGenSuppress("GetAssistantClient")]
4749
[CodeGenSuppress("GetAudioClient")]
@@ -58,6 +60,7 @@ namespace OpenAI;
5860
[CodeGenSuppress("GetLegacyCompletionClient")]
5961
[CodeGenSuppress("GetModelClient")]
6062
[CodeGenSuppress("GetModerationClient")]
63+
[CodeGenSuppress("GetRealtimeConversationClient")]
6164
[CodeGenSuppress("GetVectorStoreClient")]
6265
public partial class OpenAIClient
6366
{
@@ -110,6 +113,7 @@ public OpenAIClient(ApiKeyCredential credential, OpenAIClientOptions options)
110113
Argument.AssertNotNull(credential, nameof(credential));
111114
options ??= new OpenAIClientOptions();
112115

116+
_keyCredential = credential;
113117
_pipeline = OpenAIClient.CreatePipeline(credential, options);
114118
_endpoint = OpenAIClient.GetEndpoint(options);
115119
_options = options;
@@ -255,6 +259,9 @@ protected internal OpenAIClient(ClientPipeline pipeline, OpenAIClientOptions opt
255259
[Experimental("OPENAI001")]
256260
public virtual VectorStoreClient GetVectorStoreClient() => new(_pipeline, _options);
257261

262+
[Experimental("OPENAI002")]
263+
public virtual RealtimeConversationClient GetRealtimeConversationClient(string model) => new(model, _keyCredential, _options);
264+
258265
internal static ClientPipeline CreatePipeline(ApiKeyCredential credential, OpenAIClientOptions options)
259266
{
260267
return ClientPipeline.Create(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Development notes for .NET `/realtime` -- alpha
2+
3+
This document is intended to capture some of the exploratory design choices made when exposing the `/realtime` API in the .NET library.
4+
5+
## Naming and structure
6+
7+
"Realtime" does not describe "what" the capability does, but rather "how" it does it; `RealtimeClient`, while a faithful translation from REST, would not be descriptive or idiomatic. `AudioClient` has operations that let you send or receive audio; `ChatClient` is about all about getting chat (completion) responses; EmbeddingClient generates embeddings; `${NAME}Client` does *not* let you send or receive "realtimes."
8+
9+
A number of names could work. `Conversation` was chosen as an expedient placeholder.
10+
11+
Because the `/realtime` API involves simultaneously sending and receiving data on a single WebSocket, the primary logic vehicle is an `IDisposable` `ConversationSession` type -- this is configured by its originating `ConversationClient` and manages a `ClientWebSocket` instance. `ConversationClient` then provides task-based methods like `SendText` and `SubmitToolResponse` -- methods that allow the abstraction of client-originated request messages -- while exposing an `IAsyncEnumerable` collection of (response) `ConversationMessage` instances via `ReceiveMessagesAsync`.
12+
13+
The initial design approach for `ConversationMessage` feature uses a "squish" strategy; the many variant concrete message types are internalized, then composed into the single wrapper that conditionally populates appropriate properties based on the underlying message. This is a reapplication of the general principles applied to Chat Completion and Assistants streaming, though it's a larger single-type "squish" than previously pursued.
14+
15+
This is intended to facilitate a low barrier to entry, as explicit knowledge about different message types is not necessary to work with the operation. For example, a basic "hello world" may just do something like the following:
16+
17+
```csharp
18+
using ConversationSession conversation = await client.StartConversationAsync();
19+
20+
await conversation.SendTextAsync("Hello, world!");
21+
22+
await foreach (ConversationMessage message in client.ReceiveMessagesAsync())
23+
{
24+
Console.Write(message.Text);
25+
}
26+
```
27+
28+
## Turn-based data buffering
29+
30+
A repeated piece of early alpha feedback was that a client-integrated mechanism to automatically accumulate incoming response data (not requiring manual, do-it-yourself accumulation) would be valuable.
31+
32+
To explore accomplishing this, `ConversationSession` includes a pair of properties, `LastTurnFullResponseText` and `LastTurnFullResponseAudio`, that will automatically be populated with accumulated data when a `turn_finished` event is received. This is consistent with the "snapshot" mechanism used in several instances within Stainless SDK libraries, which likewise feature automatically accumulated data being populated into an appropriate location.
33+
34+
As this requires visibility into the response body, automatic accumulation is only performed when using the convenience method variant of `ReceiveMessagesAsync`.
35+
36+
Because this accumulated text and (especially) audio data can quickly grow in size to hundreds of kilobytes, a client-only property for `LastTurnResponseAccumulationEnabled` is inserted into `ConversationOptions`. In contexts with many parallel operations and high sensitive to memory footprint, the setting can thus opt out of the behavior.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace OpenAI.RealtimeConversation;
6+
7+
[Experimental("OPENAI002")]
8+
internal static partial class ConversationContentModalitiesExtensions
9+
{
10+
internal static void ToInternalModalities(this ConversationContentModalities modalities, IList<InternalRealtimeRequestSessionUpdateCommandSessionModality> internalModalities)
11+
{
12+
internalModalities.Clear();
13+
if (modalities.HasFlag(ConversationContentModalities.Text))
14+
{
15+
internalModalities.Add(InternalRealtimeRequestSessionUpdateCommandSessionModality.Text);
16+
}
17+
if (modalities.HasFlag(ConversationContentModalities.Audio))
18+
{
19+
internalModalities.Add(InternalRealtimeRequestSessionUpdateCommandSessionModality.Audio);
20+
}
21+
}
22+
23+
internal static ConversationContentModalities FromInternalModalities(IEnumerable<InternalRealtimeRequestSessionUpdateCommandSessionModality> internalModalities)
24+
{
25+
ConversationContentModalities result = 0;
26+
foreach (InternalRealtimeRequestSessionUpdateCommandSessionModality internalModality in internalModalities ?? [])
27+
{
28+
if (internalModality == InternalRealtimeRequestSessionUpdateCommandSessionModality.Text)
29+
{
30+
result |= ConversationContentModalities.Text;
31+
}
32+
else if (internalModality == InternalRealtimeRequestSessionUpdateCommandSessionModality.Audio)
33+
{
34+
result |= ConversationContentModalities.Audio;
35+
}
36+
}
37+
return result;
38+
}
39+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace OpenAI.RealtimeConversation;
6+
7+
[Experimental("OPENAI002")]
8+
[Flags]
9+
public enum ConversationContentModalities : int
10+
{
11+
Text = 1 << 0,
12+
Audio = 1 << 1,
13+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace OpenAI.RealtimeConversation;
6+
7+
[Experimental("OPENAI002")]
8+
[CodeGenModel("RealtimeContentPart")]
9+
public partial class ConversationContentPart
10+
{
11+
[CodeGenMember("Type")]
12+
internal ConversationContentPartKind Type;
13+
14+
public ConversationContentPartKind Kind => Type;
15+
16+
public string TextValue =>
17+
(this as InternalRealtimeRequestTextContentPart)?.Text
18+
?? (this as InternalRealtimeResponseTextContentPart)?.Text;
19+
20+
public string AudioTranscriptValue =>
21+
(this as InternalRealtimeRequestAudioContentPart)?.Transcript
22+
?? (this as InternalRealtimeResponseAudioContentPart)?.Transcript;
23+
24+
public static ConversationContentPart FromInputText(string text)
25+
=> new InternalRealtimeRequestTextContentPart(text);
26+
public static ConversationContentPart FromInputAudioTranscript(string transcript = null) => new InternalRealtimeRequestAudioContentPart()
27+
{
28+
Transcript = transcript,
29+
};
30+
public static ConversationContentPart FromOutputText(string text)
31+
=> new InternalRealtimeResponseTextContentPart(text);
32+
public static ConversationContentPart FromOutputAudioTranscript(string transcript = null)
33+
=> new InternalRealtimeResponseAudioContentPart(transcript);
34+
35+
public static implicit operator ConversationContentPart(string text) => FromInputText(text);
36+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace OpenAI.RealtimeConversation;
6+
7+
[Experimental("OPENAI002")]
8+
[CodeGenModel("RealtimeFunctionTool")]
9+
public partial class ConversationFunctionTool : ConversationTool
10+
{
11+
[CodeGenMember("Name")]
12+
private string _name;
13+
public required string Name
14+
{
15+
get => _name;
16+
set => _name = value;
17+
}
18+
19+
[CodeGenMember("Description")]
20+
private string _description;
21+
22+
public string Description
23+
{
24+
get => _description;
25+
set => _description = value;
26+
}
27+
28+
[CodeGenMember("Parameters")]
29+
private BinaryData _parameters;
30+
31+
public BinaryData Parameters
32+
{
33+
get => _parameters;
34+
set => _parameters = value;
35+
}
36+
37+
public ConversationFunctionTool() : base(ConversationToolKind.Function, null)
38+
{
39+
}
40+
41+
[SetsRequiredMembers]
42+
public ConversationFunctionTool(string name)
43+
: this(ConversationToolKind.Function, null, name, null, null)
44+
{
45+
Argument.AssertNotNull(name, nameof(name));
46+
}
47+
48+
[SetsRequiredMembers]
49+
internal ConversationFunctionTool(ConversationToolKind kind, IDictionary<string, BinaryData> serializedAdditionalRawData, string name, string description, BinaryData parameters) : base(kind, serializedAdditionalRawData)
50+
{
51+
_name = name;
52+
_description = description;
53+
_parameters = parameters;
54+
}
55+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Collections.Generic;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Linq;
4+
5+
namespace OpenAI.RealtimeConversation;
6+
7+
[Experimental("OPENAI002")]
8+
[CodeGenModel("RealtimeRequestItem")]
9+
public partial class ConversationItem
10+
{
11+
public string FunctionCallId => (this as InternalRealtimeRequestFunctionCallItem)?.CallId;
12+
public string FunctionName => (this as InternalRealtimeRequestFunctionCallItem)?.Name;
13+
public string FunctionArguments => (this as InternalRealtimeRequestFunctionCallItem)?.Arguments;
14+
15+
public IReadOnlyList<ConversationContentPart> MessageContentParts
16+
=> (this as InternalRealtimeRequestAssistantMessageItem)?.Content.ToList().AsReadOnly()
17+
?? (this as InternalRealtimeRequestSystemMessageItem)?.Content?.ToList().AsReadOnly()
18+
?? (this as InternalRealtimeRequestUserMessageItem)?.Content?.ToList().AsReadOnly();
19+
public ConversationMessageRole? MessageRole
20+
=> (this as InternalRealtimeRequestMessageItem)?.Role;
21+
22+
public static ConversationItem CreateUserMessage(IEnumerable<ConversationContentPart> contentItems)
23+
{
24+
return new InternalRealtimeRequestUserMessageItem(contentItems);
25+
}
26+
27+
public static ConversationItem CreateSystemMessage(string toolCallId, IEnumerable<ConversationContentPart> contentItems)
28+
{
29+
return new InternalRealtimeRequestSystemMessageItem(contentItems);
30+
}
31+
32+
public static ConversationItem CreateAssistantMessage(IEnumerable<ConversationContentPart> contentItems)
33+
{
34+
return new InternalRealtimeRequestAssistantMessageItem(contentItems);
35+
}
36+
37+
public static ConversationItem CreateFunctionCall(string name, string callId, string arguments)
38+
{
39+
return new InternalRealtimeRequestFunctionCallItem(name, callId, arguments);
40+
}
41+
42+
public static ConversationItem CreateFunctionCallOutput(string callId, string output)
43+
{
44+
return new InternalRealtimeRequestFunctionCallOutputItem(callId, output);
45+
}
46+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.ClientModel.Primitives;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Text.Json;
5+
6+
namespace OpenAI.RealtimeConversation;
7+
8+
public partial class ConversationMaxTokensChoice : IJsonModel<ConversationMaxTokensChoice>
9+
{
10+
void IJsonModel<ConversationMaxTokensChoice>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
11+
=> CustomSerializationHelpers.SerializeInstance(this, SerializeConversationMaxTokensChoice, writer, options);
12+
13+
ConversationMaxTokensChoice IJsonModel<ConversationMaxTokensChoice>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
14+
=> CustomSerializationHelpers.DeserializeNewInstance(this, DeserializeConversationMaxTokensChoice, ref reader, options);
15+
16+
BinaryData IPersistableModel<ConversationMaxTokensChoice>.Write(ModelReaderWriterOptions options)
17+
=> CustomSerializationHelpers.SerializeInstance(this, options);
18+
19+
ConversationMaxTokensChoice IPersistableModel<ConversationMaxTokensChoice>.Create(BinaryData data, ModelReaderWriterOptions options)
20+
=> CustomSerializationHelpers.DeserializeNewInstance(this, DeserializeConversationMaxTokensChoice, data, options);
21+
22+
string IPersistableModel<ConversationMaxTokensChoice>.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
23+
24+
internal static void SerializeConversationMaxTokensChoice(ConversationMaxTokensChoice instance, Utf8JsonWriter writer, ModelReaderWriterOptions options)
25+
{
26+
if (instance._isDefaultNullValue == true)
27+
{
28+
writer.WriteNullValue();
29+
}
30+
else if (instance._stringValue is not null)
31+
{
32+
writer.WriteStringValue(instance._stringValue);
33+
}
34+
else if (instance.NumericValue.HasValue)
35+
{
36+
writer.WriteNumberValue(instance.NumericValue.Value);
37+
}
38+
}
39+
40+
internal static ConversationMaxTokensChoice DeserializeConversationMaxTokensChoice(JsonElement element, ModelReaderWriterOptions options = null)
41+
{
42+
if (element.ValueKind == JsonValueKind.Null)
43+
{
44+
return new ConversationMaxTokensChoice(isDefaultNullValue: true);
45+
}
46+
if (element.ValueKind == JsonValueKind.String)
47+
{
48+
return new ConversationMaxTokensChoice(stringValue: element.GetString());
49+
}
50+
if (element.ValueKind == JsonValueKind.Number)
51+
{
52+
return new ConversationMaxTokensChoice(numberValue: element.GetInt32());
53+
}
54+
return null;
55+
}
56+
57+
internal static ConversationMaxTokensChoice FromBinaryData(BinaryData bytes)
58+
{
59+
if (bytes is null)
60+
{
61+
return new ConversationMaxTokensChoice(isDefaultNullValue: true);
62+
}
63+
using JsonDocument document = JsonDocument.Parse(bytes);
64+
return DeserializeConversationMaxTokensChoice(document.RootElement);
65+
}
66+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ClientModel.Primitives;
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace OpenAI.RealtimeConversation;
7+
8+
[Experimental("OPENAI002")]
9+
public partial class ConversationMaxTokensChoice
10+
{
11+
public int? NumericValue { get; }
12+
private readonly bool? _isDefaultNullValue;
13+
private readonly string _stringValue;
14+
15+
public static ConversationMaxTokensChoice CreateInfiniteMaxTokensChoice()
16+
=> new("inf");
17+
public static ConversationMaxTokensChoice CreateDefaultMaxTokensChoice()
18+
=> new(isDefaultNullValue: true);
19+
public static ConversationMaxTokensChoice CreateNumericMaxTokensChoice(int maxTokens)
20+
=> new(numberValue: maxTokens);
21+
22+
public ConversationMaxTokensChoice(int numberValue)
23+
{
24+
NumericValue = numberValue;
25+
}
26+
27+
internal ConversationMaxTokensChoice(string stringValue)
28+
{
29+
_stringValue = stringValue;
30+
}
31+
32+
internal ConversationMaxTokensChoice(bool isDefaultNullValue)
33+
{
34+
_isDefaultNullValue = true;
35+
}
36+
37+
public static implicit operator ConversationMaxTokensChoice(int maxTokens)
38+
=> CreateNumericMaxTokensChoice(maxTokens);
39+
}

0 commit comments

Comments
 (0)