Skip to content

Commit 0536fe0

Browse files
committed
feat: Make openai_realtime_dart client to strong-typed
1 parent 9c855f1 commit 0536fe0

Some content is hidden

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

59 files changed

+93633
-725
lines changed

packages/openai_dart/oas/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'dart:io';
33

44
import 'package:openapi_spec/openapi_spec.dart';
55

6-
/// Generates Chroma API client Dart code from the OpenAPI spec.
6+
/// Generates OpenAI API client Dart code from the OpenAPI spec.
77
/// Official spec: https://github.com/openai/openai-openapi/blob/master/openapi.yaml
88
void main() async {
99
final spec = OpenApi.fromFile(source: 'oas/openapi_curated.yaml');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
targets:
2+
$default:
3+
builders:
4+
source_gen|combining_builder:
5+
options:
6+
ignore_for_file:
7+
- prefer_final_parameters
8+
- require_trailing_commas
9+
- non_constant_identifier_names
10+
- unnecessary_null_checks
11+
json_serializable:
12+
options:
13+
explicit_to_json: true

packages/openai_realtime_dart/example/openai_realtime_dart_example.dart

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,29 @@ Future<void> main() async {
99
);
1010

1111
// Can set parameters ahead of connecting, either separately or all at once
12-
client.updateSession(instructions: 'You are a great, upbeat friend.');
13-
client.updateSession(voice: 'alloy');
14-
client.updateSession(
15-
turnDetection: {'type': 'none'},
16-
inputAudioTranscription: {'model': 'whisper-1'},
12+
await client.updateSession(instructions: 'You are a great, upbeat friend.');
13+
await client.updateSession(voice: Voice.alloy);
14+
await client.updateSession(
15+
turnDetection: null,
16+
inputAudioTranscription:
17+
const InputAudioTranscriptionConfig(model: 'whisper-1'),
1718
);
1819

1920
// Set up event handling
20-
client.on('conversation.updated', (event) {
21+
client.on(RealtimeEventType.conversationUpdated, (e) {
22+
final event = e as RealtimeEventConversationUpdated;
2123
// item is the current item being updated
22-
final item = event?['item'];
24+
final item = event.result.item;
2325
// delta can be null or populated
24-
final delta = event?['delta'];
26+
final delta = event.result.delta;
2527
// you can fetch a full list of items at any time
2628
});
2729

2830
// Connect to Realtime API
2931
await client.connect();
3032

3133
// Send a item and triggers a generation
32-
client.sendUserMessageContent([
33-
{'type': 'input_text', 'text': 'How are you?'},
34+
await client.sendUserMessageContent(const [
35+
ContentPart.text(text: 'How are you?'),
3436
]);
3537
}

packages/openai_realtime_dart/lib/openai_realtime_dart.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export 'src/api.dart';
55
export 'src/client.dart';
66
export 'src/conversation.dart';
77
export 'src/event_handler.dart';
8+
export 'src/schema/schema.dart';

packages/openai_realtime_dart/lib/src/api.dart

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:web_socket_channel/status.dart' as status;
77
import 'package:web_socket_channel/web_socket_channel.dart';
88

99
import 'event_handler.dart';
10+
import 'schema/generated/schema/schema.dart';
1011
import 'utils.dart';
1112
import 'web_socket/web_socket.dart';
1213

@@ -76,15 +77,27 @@ class RealtimeAPI extends RealtimeEventHandler {
7677
_ws!.stream.listen(
7778
(data) {
7879
final message = json.decode(data) as Map<String, dynamic>;
79-
receive(message['type'], message);
80+
receive(message);
8081
},
8182
onError: (dynamic error) {
8283
_log.severe('Error', error);
83-
dispatch('close', {'error': true});
84+
dispatch(
85+
RealtimeEventType.close,
86+
RealtimeEvent.close(
87+
eventId: RealtimeUtils.generateId(),
88+
error: true,
89+
),
90+
);
8491
},
8592
onDone: () {
8693
_log.info('Disconnected from "$url"');
87-
dispatch('close', {'error': false});
94+
dispatch(
95+
RealtimeEventType.close,
96+
RealtimeEvent.close(
97+
eventId: RealtimeUtils.generateId(),
98+
error: false,
99+
),
100+
);
88101
},
89102
);
90103

@@ -119,42 +132,69 @@ class RealtimeAPI extends RealtimeEventHandler {
119132
}
120133

121134
/// Receives an event from WebSocket and dispatches as
122-
/// "server.{eventName}" and "server.*" events.
123-
void receive(String eventName, Map<String, dynamic> event) {
124-
_logEvent(eventName, event, fromClient: false);
125-
dispatch('server.$eventName', event);
126-
dispatch('server.*', event);
135+
/// "[RealtimeEventType]" and "[RealtimeEventType.serverAll]" events.
136+
Future<void> receive(Map<String, dynamic> eventData) async {
137+
final event = RealtimeEvent.fromJson(eventData);
138+
_logEvent(event, fromClient: false);
139+
await dispatch(event.type, event);
140+
await dispatch(RealtimeEventType.serverAll, event);
141+
await dispatch(RealtimeEventType.all, event);
127142
}
128143

129-
/// Sends an event to WebSocket and dispatches as "client.{eventName}"
130-
/// and "client.*" events.
131-
void send(String eventName, [Map<String, dynamic>? data]) {
144+
/// Sends an event to WebSocket and dispatches as "[RealtimeEventType]"
145+
/// and "[RealtimeEventType.clientAll]" events.
146+
Future<void> send(RealtimeEvent event) async {
132147
if (!isConnected()) {
133148
throw Exception('RealtimeAPI is not connected');
134149
}
135150

136-
final event = {
137-
'event_id': RealtimeUtils.generateId('evt_'),
138-
'type': eventName,
139-
...?data,
140-
};
151+
final finalEvent = event.copyWith(
152+
eventId: RealtimeUtils.generateId(),
153+
);
141154

142-
dispatch('client.$eventName', event);
143-
dispatch('client.*', event);
144-
_logEvent(eventName, event, fromClient: true);
155+
_logEvent(finalEvent, fromClient: true);
156+
await dispatch(finalEvent.type, finalEvent);
157+
await dispatch(RealtimeEventType.clientAll, finalEvent);
158+
await dispatch(RealtimeEventType.all, finalEvent);
145159

146-
_ws!.sink.add(json.encode(event));
160+
final data = json.encode(finalEvent.toJson());
161+
_ws!.sink.add(data);
147162
}
148163

149164
void _logEvent(
150-
String name,
151-
Map<String, dynamic> event, {
165+
RealtimeEvent event, {
152166
required bool fromClient,
153167
}) {
154-
final eventString = event.toString();
155-
final eventLength = eventString.length;
156-
final eventFormatted =
157-
eventString.substring(0, eventLength > 200 ? 200 : eventLength);
158-
_log.info('${fromClient ? 'sent' : 'received'}: $name $eventFormatted');
168+
if (!debug) {
169+
return;
170+
}
171+
172+
final eventJson = event.toJson();
173+
174+
// Recursive function to replace "audio" property content
175+
void replaceAudioProperty(dynamic json) {
176+
if (json is Map<String, dynamic>) {
177+
json.forEach((key, value) {
178+
if (key == 'audio' ||
179+
(key == 'delta' && json['type'] == 'response.audio.delta')) {
180+
json[key] = 'base64-encoded-audio';
181+
} else {
182+
replaceAudioProperty(value);
183+
}
184+
});
185+
} else if (json is List) {
186+
for (var i = 0; i < json.length; i++) {
187+
replaceAudioProperty(json[i]);
188+
}
189+
}
190+
}
191+
192+
// Replace "audio" property content in the event JSON
193+
replaceAudioProperty(eventJson);
194+
195+
final eventString = jsonEncode(eventJson);
196+
_log.info(
197+
'${fromClient ? 'sent' : 'received'}: ${event.type.name} $eventString',
198+
);
159199
}
160200
}

0 commit comments

Comments
 (0)