Skip to content

Commit

Permalink
feat: Make openai_realtime_dart client to strong-typed
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmigloz committed Oct 26, 2024
1 parent 9c855f1 commit 0536fe0
Show file tree
Hide file tree
Showing 59 changed files with 93,633 additions and 725 deletions.
2 changes: 1 addition & 1 deletion packages/openai_dart/oas/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:io';

import 'package:openapi_spec/openapi_spec.dart';

/// Generates Chroma API client Dart code from the OpenAPI spec.
/// Generates OpenAI API client Dart code from the OpenAPI spec.
/// Official spec: https://github.com/openai/openai-openapi/blob/master/openapi.yaml
void main() async {
final spec = OpenApi.fromFile(source: 'oas/openapi_curated.yaml');
Expand Down
13 changes: 13 additions & 0 deletions packages/openai_realtime_dart/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
targets:
$default:
builders:
source_gen|combining_builder:
options:
ignore_for_file:
- prefer_final_parameters
- require_trailing_commas
- non_constant_identifier_names
- unnecessary_null_checks
json_serializable:
options:
explicit_to_json: true
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,29 @@ Future<void> main() async {
);

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

// Set up event handling
client.on('conversation.updated', (event) {
client.on(RealtimeEventType.conversationUpdated, (e) {
final event = e as RealtimeEventConversationUpdated;
// item is the current item being updated
final item = event?['item'];
final item = event.result.item;
// delta can be null or populated
final delta = event?['delta'];
final delta = event.result.delta;
// you can fetch a full list of items at any time
});

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

// Send a item and triggers a generation
client.sendUserMessageContent([
{'type': 'input_text', 'text': 'How are you?'},
await client.sendUserMessageContent(const [
ContentPart.text(text: 'How are you?'),
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export 'src/api.dart';
export 'src/client.dart';
export 'src/conversation.dart';
export 'src/event_handler.dart';
export 'src/schema/schema.dart';
94 changes: 67 additions & 27 deletions packages/openai_realtime_dart/lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:web_socket_channel/status.dart' as status;
import 'package:web_socket_channel/web_socket_channel.dart';

import 'event_handler.dart';
import 'schema/generated/schema/schema.dart';
import 'utils.dart';
import 'web_socket/web_socket.dart';

Expand Down Expand Up @@ -76,15 +77,27 @@ class RealtimeAPI extends RealtimeEventHandler {
_ws!.stream.listen(
(data) {
final message = json.decode(data) as Map<String, dynamic>;
receive(message['type'], message);
receive(message);
},
onError: (dynamic error) {
_log.severe('Error', error);
dispatch('close', {'error': true});
dispatch(
RealtimeEventType.close,
RealtimeEvent.close(
eventId: RealtimeUtils.generateId(),
error: true,
),
);
},
onDone: () {
_log.info('Disconnected from "$url"');
dispatch('close', {'error': false});
dispatch(
RealtimeEventType.close,
RealtimeEvent.close(
eventId: RealtimeUtils.generateId(),
error: false,
),
);
},
);

Expand Down Expand Up @@ -119,42 +132,69 @@ class RealtimeAPI extends RealtimeEventHandler {
}

/// Receives an event from WebSocket and dispatches as
/// "server.{eventName}" and "server.*" events.
void receive(String eventName, Map<String, dynamic> event) {
_logEvent(eventName, event, fromClient: false);
dispatch('server.$eventName', event);
dispatch('server.*', event);
/// "[RealtimeEventType]" and "[RealtimeEventType.serverAll]" events.
Future<void> receive(Map<String, dynamic> eventData) async {
final event = RealtimeEvent.fromJson(eventData);
_logEvent(event, fromClient: false);
await dispatch(event.type, event);
await dispatch(RealtimeEventType.serverAll, event);
await dispatch(RealtimeEventType.all, event);
}

/// Sends an event to WebSocket and dispatches as "client.{eventName}"
/// and "client.*" events.
void send(String eventName, [Map<String, dynamic>? data]) {
/// Sends an event to WebSocket and dispatches as "[RealtimeEventType]"
/// and "[RealtimeEventType.clientAll]" events.
Future<void> send(RealtimeEvent event) async {
if (!isConnected()) {
throw Exception('RealtimeAPI is not connected');
}

final event = {
'event_id': RealtimeUtils.generateId('evt_'),
'type': eventName,
...?data,
};
final finalEvent = event.copyWith(
eventId: RealtimeUtils.generateId(),
);

dispatch('client.$eventName', event);
dispatch('client.*', event);
_logEvent(eventName, event, fromClient: true);
_logEvent(finalEvent, fromClient: true);
await dispatch(finalEvent.type, finalEvent);
await dispatch(RealtimeEventType.clientAll, finalEvent);
await dispatch(RealtimeEventType.all, finalEvent);

_ws!.sink.add(json.encode(event));
final data = json.encode(finalEvent.toJson());
_ws!.sink.add(data);
}

void _logEvent(
String name,
Map<String, dynamic> event, {
RealtimeEvent event, {
required bool fromClient,
}) {
final eventString = event.toString();
final eventLength = eventString.length;
final eventFormatted =
eventString.substring(0, eventLength > 200 ? 200 : eventLength);
_log.info('${fromClient ? 'sent' : 'received'}: $name $eventFormatted');
if (!debug) {
return;
}

final eventJson = event.toJson();

// Recursive function to replace "audio" property content
void replaceAudioProperty(dynamic json) {
if (json is Map<String, dynamic>) {
json.forEach((key, value) {
if (key == 'audio' ||
(key == 'delta' && json['type'] == 'response.audio.delta')) {
json[key] = 'base64-encoded-audio';
} else {
replaceAudioProperty(value);
}
});
} else if (json is List) {
for (var i = 0; i < json.length; i++) {
replaceAudioProperty(json[i]);
}
}
}

// Replace "audio" property content in the event JSON
replaceAudioProperty(eventJson);

final eventString = jsonEncode(eventJson);
_log.info(
'${fromClient ? 'sent' : 'received'}: ${event.type.name} $eventString',
);
}
}
Loading

0 comments on commit 0536fe0

Please sign in to comment.