-
Notifications
You must be signed in to change notification settings - Fork 22
feat: OpenAI realtime api #935
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
base: main
Are you sure you want to change the base?
Changes from all commits
6f303a3
ecd953e
71d4a4c
232725d
6b872d4
54b06a5
df2098a
80a5d3e
d6ad031
2ed77e0
c212c1e
d144a3a
03d1e70
5415795
a0cd474
25d673b
8dc8ae6
4240910
0603218
fcebe1e
9a6d251
de69739
0f2cd62
6082105
0c2038d
537203d
7a5220e
e7d844d
7c6dced
eb8a451
671f960
625f72a
b4ba471
85120c1
fa4bb64
e4e04b3
473a3e2
94a0eef
5fbe0da
50392d6
86cd068
e95a5df
ac27bb7
e38a42a
4b789d3
c4f597b
611c0eb
b90ab43
4ad44ff
dc53160
c6a0e94
1400efc
86ab775
51b35a0
f92d327
a0e5eef
ff3007b
2060668
1e74df5
fc6bd75
0fb2646
54b151d
f6698c5
f36c8c7
40971d4
7f656aa
7eed889
f7def36
a3d4641
bd8cb90
4d7cf2c
7e008bb
b36ed95
143c01f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.sap.ai.sdk.core; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
|
|
||
| /** Represents possible configuration params of realtime client */ | ||
| public interface RealtimeParam { | ||
| /** Represents configurable options */ | ||
| enum SpeechOutputParamName { | ||
| /** Voice name to use to produce sound */ | ||
| VOICE, | ||
| /** | ||
| * How model will recognize that it is its turn to respond (e.g. explicitly asked, automatically | ||
| * detected) | ||
| */ | ||
| TURN_DETECTION, | ||
| } | ||
|
|
||
| /** | ||
| * Returns param name | ||
| * | ||
| * @return name | ||
| */ | ||
| @Nonnull | ||
| SpeechOutputParamName getParamName(); | ||
|
|
||
| /** | ||
| * Returns string value representation of the param | ||
| * | ||
| * @return string value | ||
| */ | ||
| @Nonnull | ||
| String getValueAsString(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.sap.ai.sdk.core; | ||
|
|
||
| import java.util.Objects; | ||
| import javax.annotation.Nonnull; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** Allows to configure turn detection (how model responds). */ | ||
| public final class RealtimeParamTurnDetection implements RealtimeParam { | ||
|
|
||
| /** Model tries to recognize if/when it should respond automatically */ | ||
| public static final RealtimeParamTurnDetection BY_MODEL_AUTO = | ||
| new RealtimeParamTurnDetection("BY_MODEL_AUTO"); | ||
|
|
||
| /** | ||
| * Each call to the provided realtime client is considered a turn (eager explicit turn detection). | ||
| * Less convenient than the automatic option but may give lower latency in some cases (model does | ||
| * not need to perform additional turn detection analysis). | ||
| */ | ||
| public static final RealtimeParamTurnDetection EACH_CALL_IS_A_TURN = | ||
| new RealtimeParamTurnDetection("EACH_CALL_IS_A_TURN"); | ||
|
|
||
| private final String turnDetectionKind; | ||
|
|
||
| private RealtimeParamTurnDetection(final String turnDetectionKind) { | ||
| this.turnDetectionKind = turnDetectionKind; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nonnull SpeechOutputParamName getParamName() { | ||
| return SpeechOutputParamName.TURN_DETECTION; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nonnull String getValueAsString() { | ||
| return turnDetectionKind; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(@Nullable final Object o) { | ||
| if (o == null || getClass() != o.getClass()) { | ||
| return false; | ||
| } | ||
| final RealtimeParamTurnDetection that = (RealtimeParamTurnDetection) o; | ||
| return Objects.equals(turnDetectionKind, that.turnDetectionKind); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(turnDetectionKind); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.sap.ai.sdk.core; | ||
|
|
||
| import java.util.Objects; | ||
| import javax.annotation.Nonnull; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** Allows to configure model output voice */ | ||
| public final class RealtimeParamVoice implements RealtimeParam { | ||
|
|
||
| /** Standard voice 1 */ | ||
| public static final RealtimeParamVoice DEFAULT_1 = new RealtimeParamVoice("DEFAULT_1"); | ||
|
|
||
| /** Standard voice 2 */ | ||
| public static final RealtimeParamVoice DEFAULT_2 = new RealtimeParamVoice("DEFAULT_2"); | ||
|
|
||
| private final String voice; | ||
|
|
||
| private RealtimeParamVoice(@Nonnull final String voice) { | ||
| this.voice = voice; | ||
| } | ||
|
|
||
| /** | ||
| * Allows to configure raw voice name as named by model provider. Unsafe because SDK cannot verify | ||
| * in advance if the provided voice name is correct and supported by the chosen model and use case | ||
| * | ||
| * @param voiceName as named by model provider | ||
| * @return typed voice client configuration param | ||
| */ | ||
| @Nonnull | ||
| public static RealtimeParamVoice unsafeWithExplicitVoice(@Nonnull final String voiceName) { | ||
| return new RealtimeParamVoice(voiceName); | ||
| } | ||
|
|
||
| @Override | ||
| public @Nonnull SpeechOutputParamName getParamName() { | ||
| return SpeechOutputParamName.VOICE; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nonnull String getValueAsString() { | ||
| return voice; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(@Nullable final Object o) { | ||
| if (o == null || getClass() != o.getClass()) { | ||
| return false; | ||
| } | ||
| final RealtimeParamVoice that = (RealtimeParamVoice) o; | ||
| return Objects.equals(voice, that.voice); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(voice); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.sap.ai.sdk.foundationmodels.openai; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Minor) We had a discussion that it could be great to put every class in its own realtime folder to make it easier to maintain and not make the same mistake that we did in the orchestration package that became very crowded.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missed/forgot the part about the dedicated sub-package thanks, now moved! |
||
|
|
||
| /** | ||
| * Functional interface representing audio input channel (audio data consumer) | ||
| * | ||
| * <p>Should be closed by application (try-with-resources) when not needed anymore | ||
| */ | ||
| public interface AudioInputChannel extends AutoCloseable { | ||
|
|
||
| /** | ||
| * This method is sequentially invoked by audio data provider to supply implementer (consumer) | ||
| * with the audio data. Exact audio format (encoding, sampling rate, etc.) depends on the usage | ||
| * context | ||
| * | ||
| * @param rawBytesChunk binary data in the depending on the use case format | ||
| */ | ||
| void inputAudio(byte[] rawBytesChunk); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.sap.ai.sdk.foundationmodels.openai; | ||
|
|
||
| /** Functional interface representing audio output channel (audio data consumer) */ | ||
| public interface AudioOutputChannel { | ||
|
|
||
| /** | ||
| * This method is sequentially invoked by audio data provider to supply implementer (consumer) | ||
| * with the audio data. Exact audio format (encoding, sampling rate, etc.) depends on the usage | ||
| * context | ||
| * | ||
| * @param rawBytesChunk binary data in the depending on the use case format | ||
| * @param isLast true if this call logically concludes previous and this passed bytes data into a | ||
| * single logical entity (e.g. gets called at the end when all byte parts of a single message | ||
| * get passed) | ||
| */ | ||
| void outputAudio(byte[] rawBytesChunk, boolean isLast); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.sap.ai.sdk.foundationmodels.openai; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
|
|
||
| /** | ||
| * Allows to input (send) text to the open channel, must be closed when not needed anymore (e.g. | ||
| * try-with-resources) | ||
| */ | ||
| public interface TextInputChannel extends AutoCloseable { | ||
|
|
||
| /** | ||
| * Sends input text | ||
| * | ||
| * @param text text to send | ||
| */ | ||
| void sendText(@Nonnull final String text); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.sap.ai.sdk.foundationmodels.openai; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
|
|
||
| /** Functional interface representing text output channel (text data consumer) */ | ||
| public interface TextOutputChannel { | ||
|
|
||
| /** | ||
| * This method is sequentially invoked by text data provider to supply implementer (consumer) with | ||
| * the text data. | ||
| * | ||
| * @param textChunk chunk of the text (possibly partial content) | ||
| * @param isLast true if this call logically concludes previous and this passed text data into a | ||
| * single logical entity (e.g. gets called at the end when all parts of a single message get | ||
| * passed) | ||
| */ | ||
| void outputText(@Nonnull final CharSequence textChunk, final boolean isLast); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package com.sap.ai.sdk.foundationmodels.openai.realtime; | ||
|
|
||
| import java.net.http.WebSocket; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.CompletionStage; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.function.Consumer; | ||
| import javax.annotation.Nonnull; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| class BufferedWebSocketListener implements WebSocket.Listener { | ||
|
|
||
| private final Consumer<WebSocket> onOpen; | ||
| private final BiConsumer<WebSocket, CharSequence> onText; | ||
| private final StringBuilder buffer; | ||
|
|
||
| BufferedWebSocketListener( | ||
| @Nonnull final Consumer<WebSocket> onOpen, | ||
| @Nonnull final BiConsumer<WebSocket, CharSequence> onText) { | ||
| this.onOpen = onOpen; | ||
| this.onText = onText; | ||
| this.buffer = new StringBuilder(128 * 1024); | ||
| } | ||
|
|
||
| @Override | ||
| public void onOpen(@Nonnull final WebSocket webSocket) { | ||
| this.onOpen.accept(webSocket); | ||
| webSocket.request(1); | ||
| } | ||
|
|
||
| @Override | ||
| @Nonnull | ||
| public CompletionStage<?> onText( | ||
| final @Nonnull WebSocket webSocket, final @Nonnull CharSequence data, final boolean isLast) { | ||
| buffer.append(data); | ||
| webSocket.request(1); | ||
| if (isLast) { | ||
| final var completeMessage = buffer.toString(); | ||
| buffer.setLength(0); | ||
| this.onText.accept(webSocket, completeMessage); | ||
| } | ||
|
|
||
| return CompletableFuture.completedStage(null); | ||
| } | ||
|
|
||
| @Override | ||
| public void onError(@Nonnull final WebSocket webSocket, @Nonnull final Throwable error) { | ||
| log.error("Websocket error occurred during realtime communication", error); | ||
| } | ||
|
|
||
| @Override | ||
| @Nonnull | ||
| public CompletionStage<?> onBinary( | ||
| @Nonnull final WebSocket webSocket, @Nonnull final ByteBuffer data, final boolean isLast) { | ||
| log.warn("Received unexpected binary bytes for WebSocket connection"); | ||
| return CompletableFuture.completedStage(null); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are realtime classes in the core module.
If they cannot be used outside of the realtime client they belong in the same folder as the realtime client