Skip to content

Commit 59e46c6

Browse files
authored
Add client tools (#7)
Also: * mute/unmute support * Simplify by removing UltravoxSessionState * Add documentation
1 parent e6463e3 commit 59e46c6

File tree

8 files changed

+322
-99
lines changed

8 files changed

+322
-99
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ gradle-app.setting
2424
.idea/other.xml
2525
.idea/workspace.xml
2626
.idea/tasks.xml
27+
local.properties
2728

demoapp/src/main/java/ai/ultravox/demoapp/MainActivity.kt

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package ai.ultravox.demoapp
33
import ai.ultravox.Transcript
44
import ai.ultravox.UltravoxSession
55
import android.Manifest.permission.RECORD_AUDIO
6+
import android.annotation.SuppressLint
67
import android.content.pm.PackageManager
78
import android.os.Bundle
9+
import android.view.View
810
import android.widget.Button
911
import android.widget.EditText
1012
import android.widget.Toast
@@ -17,9 +19,12 @@ import androidx.core.view.ViewCompat
1719
import androidx.core.view.WindowInsetsCompat
1820
import androidx.lifecycle.lifecycleScope
1921

22+
@SuppressLint("SetTextI18n")
2023
class MainActivity : AppCompatActivity() {
2124

2225
private lateinit var joinButton: Button
26+
private lateinit var muteMicButton: Button
27+
private lateinit var muteSpeakerButton: Button
2328
private lateinit var joinText: EditText
2429
private lateinit var session: UltravoxSession
2530
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
@@ -46,9 +51,15 @@ class MainActivity : AppCompatActivity() {
4651
}
4752
}
4853
joinButton = findViewById(R.id.join_button)
54+
muteMicButton = findViewById(R.id.mic_mute_button)
55+
muteSpeakerButton = findViewById(R.id.speaker_mute_button)
4956
joinText = findViewById(R.id.join_url_text)
5057
session = UltravoxSession(applicationContext, lifecycleScope)
5158
joinButton.setOnClickListener { onJoinClicked() }
59+
muteMicButton.visibility = View.INVISIBLE
60+
muteSpeakerButton.visibility = View.INVISIBLE
61+
muteMicButton.setOnClickListener { onMicMute() }
62+
muteSpeakerButton.setOnClickListener { onSpeakerMute() }
5263
}
5364

5465
private fun onJoinClicked() {
@@ -63,27 +74,57 @@ class MainActivity : AppCompatActivity() {
6374
}
6475
}
6576

77+
private fun onLeaveClicked() {
78+
session.leaveCall()
79+
joinButton.text = "Join"
80+
joinButton.setOnClickListener { onJoinClicked() }
81+
muteMicButton.visibility = View.INVISIBLE
82+
muteSpeakerButton.visibility = View.INVISIBLE
83+
}
84+
85+
private fun onMicMute() {
86+
session.toggleMicMuted()
87+
if (session.micMuted) {
88+
muteMicButton.text = "Unmute User"
89+
} else {
90+
muteMicButton.text = "Mute User"
91+
}
92+
}
93+
94+
private fun onSpeakerMute() {
95+
session.toggleSpeakerMuted()
96+
if (session.speakerMuted) {
97+
muteSpeakerButton.text = "Unmute Agent"
98+
} else {
99+
muteSpeakerButton.text = "Mute Agent"
100+
}
101+
}
102+
66103
private fun joinCall() {
67-
val sessionState = session.joinCall(joinText.text.toString())
68-
sessionState.listen("transcript") {
104+
session.listen("transcript") {
69105
run {
70-
val last = sessionState.lastTranscript
106+
val last = session.lastTranscript
71107
if (last != null && last.isFinal) {
72108
val prefix = if (last.speaker == Transcript.Role.USER) "USER: " else "AGENT: "
73109
Toast.makeText(this.applicationContext, prefix + last.text, Toast.LENGTH_LONG)
74110
.show()
75111
}
76112
}
77113
}
78-
sessionState.listen("status") {
114+
session.listen("status") {
79115
run {
80116
Toast.makeText(
81117
this.applicationContext,
82-
sessionState.status.name,
118+
session.status.name,
83119
Toast.LENGTH_SHORT
84120
).show()
85121
}
86122
}
123+
session.joinCall(joinText.text.toString())
124+
joinButton.text = "Leave"
125+
joinButton.setOnClickListener { onLeaveClicked() }
126+
muteMicButton.visibility = View.VISIBLE
127+
muteSpeakerButton.visibility = View.VISIBLE
87128
}
88129

89130
override fun onDestroy() {

demoapp/src/main/res/layout/activity_main.xml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
android:text="Enter join url:"
1515
app:layout_constraintBottom_toBottomOf="parent"
1616
app:layout_constraintEnd_toEndOf="parent"
17+
app:layout_constraintHorizontal_bias="0.498"
1718
app:layout_constraintStart_toStartOf="parent"
18-
app:layout_constraintTop_toTopOf="parent" />
19+
app:layout_constraintTop_toTopOf="parent"
20+
app:layout_constraintVertical_bias="0.056" />
1921

2022
<com.google.android.material.textfield.TextInputLayout
2123
android:id="@+id/textInputLayout"
@@ -39,11 +41,34 @@
3941
android:id="@+id/join_button"
4042
android:layout_width="88dp"
4143
android:layout_height="48dp"
42-
android:layout_marginStart="160dp"
44+
android:layout_marginStart="32dp"
4345
android:layout_marginTop="13dp"
44-
android:layout_marginEnd="160dp"
4546
android:text="Join"
4647
app:layout_constraintEnd_toEndOf="parent"
48+
app:layout_constraintHorizontal_bias="0.0"
49+
app:layout_constraintStart_toStartOf="parent"
50+
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
51+
52+
<Button
53+
android:id="@+id/mic_mute_button"
54+
android:layout_width="88dp"
55+
android:layout_height="wrap_content"
56+
android:layout_marginTop="12dp"
57+
android:text="Mute User"
58+
app:layout_constraintEnd_toEndOf="parent"
59+
app:layout_constraintHorizontal_bias="0.63"
60+
app:layout_constraintStart_toStartOf="parent"
61+
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
62+
63+
<Button
64+
android:id="@+id/speaker_mute_button"
65+
android:layout_width="88dp"
66+
android:layout_height="wrap_content"
67+
android:layout_marginTop="12dp"
68+
android:layout_marginEnd="24dp"
69+
android:text="Mute Agent"
70+
app:layout_constraintEnd_toEndOf="parent"
71+
app:layout_constraintHorizontal_bias="1.0"
4772
app:layout_constraintStart_toStartOf="parent"
4873
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
4974

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package ai.ultravox
2+
3+
/** Return type for client-implemented tools. */
4+
data class ClientToolResult(
5+
/** The result, exactly as it will be seen by the model. (Often JSON.) */
6+
val result: String,
7+
/**
8+
* For tools that affect the call instead of providing information back to the model,
9+
* responseType may be set to indicate how the call should be altered. In such cases,
10+
* result should be encoded JSON with instructions for the server. The schema depends
11+
* on the response type. See https://docs.ultravox.ai/tools/ for more details.
12+
*/
13+
val responseType: String?,
14+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package ai.ultravox
22

3+
/** A transcription of a single utterance. */
34
data class Transcript(
5+
/** The possibly-incomplete text of an utterance. */
46
val text: String,
7+
/** Whether the text is complete or the utterance is ongoing. */
58
val isFinal: Boolean,
9+
/** Who emitted the utterance. */
610
val speaker: Role,
11+
/** The medium through which the utterance was emitted. */
712
val medium: Medium,
813
) {
14+
/** The participant who created a transcript. */
915
enum class Role { USER, AGENT }
16+
17+
/** The medium by which the transcript was communicated. */
1018
enum class Medium { VOICE, TEXT }
1119
}

0 commit comments

Comments
 (0)