Skip to content

Commit 111ead5

Browse files
committed
Add ability to verbally change settings.
Signed-off-by: Katharine Berry <[email protected]>
1 parent 989c296 commit 111ead5

File tree

8 files changed

+357
-4
lines changed

8 files changed

+357
-4
lines changed

app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@
106106
"MAP_WIDGET",
107107
"MAP_WIDGET_IMAGE_ID",
108108
"MAP_WIDGET_USER_LOCATION",
109-
"CONFIRM_TRANSCRIPTS"
109+
"CONFIRM_TRANSCRIPTS",
110+
"ACTION_SETTINGS_UPDATED"
110111
],
111112
"resources": {
112113
"media": [

app/src/c/converse/conversation_manager.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,18 @@ static void prv_handle_app_message_inbox_received(DictionaryIterator *iter, void
206206
.action = {}
207207
};
208208
conversation_manager_add_action(manager, &action);
209+
} else if (tuple->key == MESSAGE_KEY_ACTION_SETTINGS_UPDATED) {
210+
char *sentence = bmalloc(strlen(tuple->value->cstring) + 1);
211+
strcpy(sentence, tuple->value->cstring);
212+
ConversationAction action = {
213+
.type = ConversationActionTypeGenericSentence,
214+
.action = {
215+
.generic_sentence = {
216+
.sentence = sentence,
217+
}
218+
},
219+
};
220+
conversation_manager_add_action(manager, &action);
209221
} else if (tuple->key == MESSAGE_KEY_WARNING) {
210222
conversation_complete_response(manager->conversation);
211223
prv_conversation_updated(manager, false);

app/src/c/converse/segments/info_layer.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ static char* prv_generate_action_text(ConversationAction* action) {
208208
strncpy(buffer, "Checklist updated.", 50);
209209
break;
210210
case ConversationActionTypeGenericSentence:
211-
strncpy(buffer, action->action.generic_sentence.sentence, 50);
211+
free(buffer);
212+
buffer = bmalloc(strlen(action->action.generic_sentence.sentence) + 1);
213+
strcpy(buffer, action->action.generic_sentence.sentence);
212214
break;
213215
}
214216
return buffer;

app/src/pkjs/actions/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
var reminders = require('./reminders');
1818
var alarms = require('./alarms');
1919
var feedback = require('./feedback');
20+
var settings = require('./settings');
2021
var config = require('../config.js');
2122

2223
var actionMap = {
@@ -26,6 +27,7 @@ var actionMap = {
2627
'set_alarm': alarms.setAlarm,
2728
'get_alarm': alarms.getAlarm,
2829
'send_feedback': feedback.sendFeedback,
30+
'update_settings': settings.updateSettings,
2931
};
3032

3133
var extraActions = ['named_alarms'];

app/src/pkjs/actions/settings.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
var config = require('../config');
18+
19+
function settingUpdater(session, key, value) {
20+
var updaters = {
21+
'unitSystem': updateUnitSystem,
22+
'responseLanguage': updateResponseLanguage,
23+
'alarmVibrationPattern': updateAlarmVibrationPattern,
24+
'timerVibrationPattern': updateTimerVibrationPattern,
25+
'quickLaunchBehaviour': updateQuickLaunchBehaviour,
26+
'confirmPrompts': updateConfirmPrompts,
27+
};
28+
if (key in updaters) {
29+
return updaters[key](session, value);
30+
} else {
31+
console.log("Unknown setting: " + key);
32+
}
33+
return null;
34+
}
35+
36+
function updateUnitSystem(session, value) {
37+
if (value === 'imperial') {
38+
config.setSetting('UNIT_PREFERENCE', 'imperial');
39+
return 'units to imperial';
40+
} else if (value === 'metric') {
41+
config.setSetting('UNIT_PREFERENCE', 'metric');
42+
return 'units to metric';
43+
} else if (value === 'uk hybrid') {
44+
config.setSetting('UNIT_PREFERENCE', 'uk');
45+
return 'units to UK hybrid';
46+
} else if (value === 'both') {
47+
config.setSetting('UNIT_PREFERENCE', 'both');
48+
return 'units to both imperial and metric';
49+
} else if (value === 'auto') {
50+
config.setSetting('UNIT_PREFERENCE', '');
51+
return 'units to automatic';
52+
} else {
53+
console.log("Unknown unit system: " + value);
54+
return null;
55+
}
56+
}
57+
58+
function updateResponseLanguage(session, value) {
59+
var knownLanguageMap = {
60+
"af_ZA": "Afrikaans",
61+
"id_ID": "Bahasa Indonesia",
62+
"ms_MY": "Bahasa Melayu",
63+
"cs_CZ": "Čeština",
64+
"da_DK": "Dansk",
65+
"de_DE": "Deutsch",
66+
"en_US": "English",
67+
"es_ES": "Español",
68+
"fil_PH": "Filipino",
69+
"fr_FR": "Français",
70+
"gl_ES": "Galego",
71+
"hr_HR": "Hrvatski",
72+
"is_IS": "Íslenska",
73+
"it_IT": "Italiano",
74+
"sw_TZ": "Kiswahili",
75+
"lv_LV": "Latviešu",
76+
"lt_LT": "Lietuvių",
77+
"hu_HU": "Magyar",
78+
"nl_NL": "Nederlands",
79+
"no_NO": "Norsk",
80+
"pl_PL": "Polski",
81+
"pt_PT": "Português",
82+
"ro_RO": "Română",
83+
"ru_RU": "Русский",
84+
"sk_SK": "Slovenčina",
85+
"sl_SI": "Slovenščina",
86+
"fi_FI": "Suomi",
87+
"sv_SE": "Svenska",
88+
"tr_TR": "Türkçe",
89+
"zu_ZA": "IsiZulu"
90+
}
91+
if (value === 'auto') {
92+
config.setSetting("LANGUAGE_CODE", "");
93+
return 'response language to automatic';
94+
} else if (value in knownLanguageMap) {
95+
config.setSetting("LANGUAGE_CODE", value);
96+
return 'response language to ' + knownLanguageMap[value];
97+
} else {
98+
console.log("Unknown response language: " + value);
99+
return null;
100+
}
101+
}
102+
103+
var vibePatternMap = {
104+
'Reveille': 1,
105+
'Mario': 2,
106+
'Nudge Nudge': 3,
107+
'Jackhammer': 4,
108+
'Standard': 5,
109+
}
110+
111+
function updateAlarmVibrationPattern(session, value) {
112+
if (value in vibePatternMap) {
113+
config.setSetting('ALARM_VIBRATION_PATTERN', vibePatternMap[value]);
114+
session.enqueue({
115+
'ALARM_VIBRATION_PATTERN': vibePatternMap[value]
116+
});
117+
return 'alarm vibration pattern to ' + value;
118+
} else {
119+
console.log("Unknown alarm vibration pattern: " + value);
120+
}
121+
}
122+
123+
function updateTimerVibrationPattern(session, value) {
124+
if (value in vibePatternMap) {
125+
config.setSetting('TIMER_VIBRATION_PATTERN', vibePatternMap[value]);
126+
session.enqueue({
127+
'ALARM_VIBRATION_PATTERN': vibePatternMap[value]
128+
});
129+
return 'timer vibration pattern to ' + value;
130+
} else {
131+
console.log("Unknown timer vibration pattern: " + value);
132+
}
133+
}
134+
135+
function updateQuickLaunchBehaviour(session, value) {
136+
var behaviourMap = {
137+
'start conversation and time out': 1,
138+
'start conversation and stay open': 2,
139+
'open home screen': 3,
140+
};
141+
if (value in behaviourMap) {
142+
config.setSetting('QUICK_LAUNCH_BEHAVIOUR', behaviourMap[value]);
143+
session.enqueue({
144+
'QUICK_LAUNCH_BEHAVIOUR': behaviourMap[value]
145+
});
146+
return 'quick launch behaviour to ' + value;
147+
} else {
148+
console.log("Unknown quick launch behaviour: " + value);
149+
}
150+
}
151+
152+
function updateConfirmPrompts(session, value) {
153+
config.setSetting('CONFIRM_TRANSCRIPTS', value);
154+
session.enqueue({
155+
'CONFIRM_TRANSCRIPTS': value
156+
});
157+
if (value) {
158+
return 'prompt confirmation to on';
159+
} else {
160+
return 'prompt confirmation to off';
161+
}
162+
}
163+
164+
exports.updateSettings = function(session, message, callback) {
165+
console.log("Updating settings...");
166+
var summaries = [];
167+
for (var key in message) {
168+
summaries.push(settingUpdater(session, key, message[key]));
169+
}
170+
var summary = summaries.filter(function(s) { return !!s; }).join(", ");
171+
if (summary === "") {
172+
summary = "No settings changed.";
173+
} else {
174+
summary = "Set " + summary + ".";
175+
}
176+
session.enqueue({
177+
ACTION_SETTINGS_UPDATED: summary,
178+
});
179+
callback({"status": "ok"});
180+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package functions
16+
17+
import (
18+
"context"
19+
"github.com/pebble-dev/bobby-assistant/service/assistant/quota"
20+
"google.golang.org/genai"
21+
"strings"
22+
)
23+
24+
type UpdateSettingsInput struct {
25+
UnitSystem string `json:"unit_system"`
26+
ResponseLanguage string `json:"response_language"`
27+
AlarmVibrationPattern string `json:"alarm_vibration_pattern"`
28+
TimerVibrationPattern string `json:"timer_vibration_pattern"`
29+
QuickLaunchBehaviour string `json:"quick_launch_behaviour"`
30+
ConfirmPrompts *bool `json:"confirm_prompts"`
31+
}
32+
33+
func init() {
34+
t := true
35+
registerFunction(Registration{
36+
Definition: genai.FunctionDeclaration{
37+
Name: "update_settings",
38+
Description: "Update the user's settings, e.g. their preferred unit system. Call if and only if the user asks you to change something. Properties not specified won't be changed. No property is required. For security reasons, changing the location permission can't be done with this method - the user must go to the settings page.",
39+
Parameters: &genai.Schema{
40+
Type: genai.TypeObject,
41+
Properties: map[string]*genai.Schema{
42+
"unit_system": {
43+
Type: genai.TypeString,
44+
Description: "Whether the user prefers metric, imperial, 'UK hybrid' (temperature in celsius, distance in miles), or both metric and imperial. Or, 'auto' to figure it out based on the user's location.",
45+
Enum: []string{"auto", "imperial", "metric", "uk hybrid", "both"},
46+
},
47+
"response_language": {
48+
Type: genai.TypeString,
49+
Description: "The user's preferred response language. This is the language in which the assistant will respond to the user, or 'automatic' to use the language of the user's last message.",
50+
Enum: []string{"auto", "af_ZA", "id_ID", "ms_MY", "cs_CZ", "da_DK", "de_DE",
51+
"en_US", "es_ES", "fil_PH", "fr_FR", "gl_ES", "hr_HR", "is_IS", "it_IT", "sw_TZ", "lv_LV",
52+
"lt_LT", "hu_HU", "nl_NL", "no_NO", "pl_PL", "pt_PT", "ro_RO", "ru_RU", "sk_SK", "sl_SI",
53+
"fi_FI", "sv_SE", "tr_TR", "zu_ZA"},
54+
},
55+
"alarm_vibration_pattern": {
56+
Type: genai.TypeString,
57+
Description: "The user's preferred alarm vibration pattern, used when alarms go off.",
58+
Enum: []string{"Reveille", "Mario", "Nudge Nudge", "Jackhammer", "Standard"},
59+
},
60+
"timer_vibration_pattern": {
61+
Type: genai.TypeString,
62+
Description: "The user's preferred timer vibration pattern, used when timers go off.",
63+
Enum: []string{"Reveille", "Mario", "Nudge Nudge", "Jackhammer", "Standard"},
64+
},
65+
"quick_launch_behaviour": {
66+
Type: genai.TypeString,
67+
Description: "The user's preferred quick launch behaviour. The app can open the home screen (same as a non-quick launch), open the conversation but time out and quit after a minute, or open the conversation and stick around.",
68+
Enum: []string{"open home screen", "start conversation and time out", "start conversation and stay open"},
69+
},
70+
"confirm_prompts": {
71+
Type: genai.TypeBoolean,
72+
Description: "Whether the user wants to be asked to confirm their all of their queries before acting on them",
73+
Nullable: &t,
74+
},
75+
},
76+
Required: []string{},
77+
},
78+
},
79+
Cb: updateSettingsImpl,
80+
Thought: updateSettingsThought,
81+
InputType: UpdateSettingsInput{},
82+
Capability: "update_settings",
83+
})
84+
}
85+
86+
func updateSettingsImpl(ctx context.Context, quotaTracker *quota.Tracker, i any, requestChan chan<- map[string]any, responseChan <-chan map[string]any) any {
87+
arg := i.(*UpdateSettingsInput)
88+
request := map[string]any{
89+
"action": "update_settings",
90+
}
91+
if arg.UnitSystem != "" {
92+
request["unitSystem"] = arg.UnitSystem
93+
}
94+
if arg.ResponseLanguage != "" {
95+
request["responseLanguage"] = arg.ResponseLanguage
96+
}
97+
if arg.AlarmVibrationPattern != "" {
98+
request["alarmVibrationPattern"] = arg.AlarmVibrationPattern
99+
}
100+
if arg.TimerVibrationPattern != "" {
101+
request["timerVibrationPattern"] = arg.TimerVibrationPattern
102+
}
103+
if arg.QuickLaunchBehaviour != "" {
104+
request["quickLaunchBehaviour"] = arg.QuickLaunchBehaviour
105+
}
106+
if arg.ConfirmPrompts != nil {
107+
request["confirmPrompts"] = *arg.ConfirmPrompts
108+
}
109+
requestChan <- request
110+
response := <-responseChan
111+
return response
112+
}
113+
114+
func updateSettingsThought(args any) string {
115+
i := args.(*UpdateSettingsInput)
116+
var settingTypes []string
117+
if i.UnitSystem != "" {
118+
settingTypes = append(settingTypes, "unit system")
119+
}
120+
if i.ResponseLanguage != "" {
121+
settingTypes = append(settingTypes, "response language")
122+
}
123+
if i.AlarmVibrationPattern != "" {
124+
settingTypes = append(settingTypes, "alarm vibration")
125+
}
126+
if i.TimerVibrationPattern != "" {
127+
settingTypes = append(settingTypes, "timer vibration")
128+
}
129+
if i.QuickLaunchBehaviour != "" {
130+
settingTypes = append(settingTypes, "quick launch behaviour")
131+
}
132+
if i.ConfirmPrompts != nil {
133+
settingTypes = append(settingTypes, "prompt confirmation")
134+
}
135+
if len(settingTypes) == 0 {
136+
return "Updating no settings..."
137+
}
138+
var settingString string
139+
if len(settingTypes) == 1 {
140+
settingString = settingTypes[0]
141+
} else {
142+
lastEntry := settingTypes[len(settingTypes)-1]
143+
settingString = strings.Join(settingTypes[0:len(settingTypes)-1], ", ") + " and " + lastEntry
144+
}
145+
return "Updating " + settingString + " settings..."
146+
}

service/assistant/session.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ func (ps *PromptSession) Run(ctx context.Context) {
356356
formattedLies = append(formattedLies, "set a timer")
357357
case "reminder":
358358
formattedLies = append(formattedLies, "set a reminder")
359+
case "settings":
360+
formattedLies = append(formattedLies, "change any settings")
359361
}
360362
}
361363
prettyLies := strings.Join(formattedLies, ", ")

0 commit comments

Comments
 (0)