Boopster commited on
Commit
fb88636
·
1 Parent(s): 1305007

feat: Introduce an onboarding review step and remove the initial welcome step from the onboarding flow.

Browse files
frontend/src/components/ComponentOverlay.tsx CHANGED
@@ -11,7 +11,7 @@ interface ComponentOverlayProps {
11
  }
12
 
13
  // Components that render full-screen (TV-distance readable)
14
- const FULLSCREEN_COMPONENTS = new Set(["HeadacheLog", "MedLog", "MedStatus", "SessionSummary", "OnboardingProgress", "OnboardingSummary", "MyMedsList", "QuickReply", "ConfirmationCard"]);
15
 
16
  export function ComponentOverlay({ component, onDismiss }: ComponentOverlayProps) {
17
  const isFullscreen = FULLSCREEN_COMPONENTS.has(component.name);
 
11
  }
12
 
13
  // Components that render full-screen (TV-distance readable)
14
+ const FULLSCREEN_COMPONENTS = new Set(["HeadacheLog", "MedLog", "MedStatus", "SessionSummary", "OnboardingProgress", "OnboardingSummary", "OnboardingReview", "MyMedsList", "QuickReply", "ConfirmationCard"]);
15
 
16
  export function ComponentOverlay({ component, onDismiss }: ComponentOverlayProps) {
17
  const isFullscreen = FULLSCREEN_COMPONENTS.has(component.name);
frontend/src/registry/OnboardingProgress.tsx CHANGED
@@ -1,16 +1,16 @@
1
  "use client";
2
 
3
- import { Check, User, Pill, Users, PartyPopper } from "lucide-react";
4
 
5
  interface OnboardingProgressProps {
6
- currentStep: "welcome" | "name" | "medications" | "contacts" | "complete";
7
  }
8
 
9
  const steps = [
10
- { id: "welcome", label: "Welcome", icon: PartyPopper },
11
  { id: "name", label: "Your Name", icon: User },
12
  { id: "medications", label: "Medications", icon: Pill },
13
  { id: "contacts", label: "Contacts", icon: Users },
 
14
  { id: "complete", label: "All Set!", icon: Check },
15
  ];
16
 
 
1
  "use client";
2
 
3
+ import { Check, User, Pill, Users, PartyPopper, ClipboardCheck } from "lucide-react";
4
 
5
  interface OnboardingProgressProps {
6
+ currentStep: "name" | "medications" | "contacts" | "review" | "complete";
7
  }
8
 
9
  const steps = [
 
10
  { id: "name", label: "Your Name", icon: User },
11
  { id: "medications", label: "Medications", icon: Pill },
12
  { id: "contacts", label: "Contacts", icon: Users },
13
+ { id: "review", label: "Review", icon: ClipboardCheck },
14
  { id: "complete", label: "All Set!", icon: Check },
15
  ];
16
 
frontend/src/registry/OnboardingReview.tsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { User, Pill, Stethoscope, Heart, ClipboardCheck, ArrowLeft, Check } from "lucide-react";
4
+
5
+ interface Medication {
6
+ medicationName: string;
7
+ dose?: string | null;
8
+ frequency?: string | null;
9
+ }
10
+
11
+ interface Contact {
12
+ name?: string | null;
13
+ email?: string | null;
14
+ }
15
+
16
+ interface OnboardingReviewProps {
17
+ displayName?: string | null;
18
+ medications?: Medication[];
19
+ neurologist?: Contact;
20
+ caregiver?: Contact;
21
+ }
22
+
23
+ export function OnboardingReview({
24
+ displayName,
25
+ medications = [],
26
+ neurologist,
27
+ caregiver,
28
+ }: OnboardingReviewProps) {
29
+ const handleConfirm = () => {
30
+ window.dispatchEvent(
31
+ new CustomEvent("confirmation-card-action", {
32
+ detail: { action: "confirm", text: "Yes, that all looks correct. Please complete my setup." },
33
+ })
34
+ );
35
+ };
36
+
37
+ const handleGoBack = () => {
38
+ window.dispatchEvent(
39
+ new CustomEvent("confirmation-card-action", {
40
+ detail: { action: "cancel", text: "I'd like to make some changes before we finalise." },
41
+ })
42
+ );
43
+ };
44
+
45
+ const hasNeurologist = neurologist?.name || neurologist?.email;
46
+ const hasCaregiver = caregiver?.name || caregiver?.email;
47
+
48
+ return (
49
+ <div
50
+ className="relative flex flex-col h-full w-full overflow-hidden px-12 py-10
51
+ animate-in zoom-in-95 fade-in duration-700"
52
+ >
53
+ {/* Top accent line */}
54
+ <div className="absolute top-0 left-[20%] right-[20%] h-1 bg-gradient-to-r from-transparent via-cta to-transparent opacity-60" />
55
+
56
+ {/* Header */}
57
+ <div className="flex items-center gap-6 mb-8">
58
+ <div
59
+ className="w-16 h-16 rounded-xl flex items-center justify-center
60
+ bg-cta/15 border border-cta/30"
61
+ >
62
+ <ClipboardCheck className="w-8 h-8 text-cta" />
63
+ </div>
64
+ <div>
65
+ <h3 className="text-3xl font-bold tracking-tight text-primary">Review Your Setup</h3>
66
+ <p className="text-lg text-muted mt-1">Check everything looks right before we finish</p>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Content sections */}
71
+ <div className="flex-1 space-y-5 overflow-y-auto pr-2">
72
+ {/* Name */}
73
+ <div className="flex items-center gap-5 p-5 rounded-2xl bg-surface-subtle/50 border border-surface-overlay">
74
+ <div className="w-12 h-12 rounded-xl bg-cta/15 flex items-center justify-center flex-shrink-0">
75
+ <User className="w-6 h-6 text-cta" />
76
+ </div>
77
+ <div>
78
+ <p className="text-sm text-muted uppercase tracking-wide font-medium">Your Name</p>
79
+ <p className={`text-2xl font-semibold ${displayName ? "text-primary" : "text-muted/50 italic"}`}>
80
+ {displayName || "Not set"}
81
+ </p>
82
+ </div>
83
+ </div>
84
+
85
+ {/* Medications */}
86
+ <div className="p-5 rounded-2xl bg-surface-subtle/50 border border-surface-overlay">
87
+ <div className="flex items-center gap-3 mb-3">
88
+ <div className="w-12 h-12 rounded-xl bg-accent-cyan/15 flex items-center justify-center flex-shrink-0">
89
+ <Pill className="w-6 h-6 text-accent-cyan" />
90
+ </div>
91
+ <div>
92
+ <p className="text-sm text-muted uppercase tracking-wide font-medium">Medications</p>
93
+ <p className="text-lg text-muted">
94
+ {medications.length > 0
95
+ ? `${medications.length} medication${medications.length !== 1 ? "s" : ""} tracked`
96
+ : "None set up"}
97
+ </p>
98
+ </div>
99
+ </div>
100
+ {medications.length > 0 && (
101
+ <div className="flex flex-wrap gap-3 mt-2 ml-15">
102
+ {medications.map((med, i) => (
103
+ <span
104
+ key={i}
105
+ className="px-4 py-2 rounded-xl bg-accent-cyan/10 border border-accent-cyan/20 text-lg font-medium text-accent-cyan"
106
+ >
107
+ {med.medicationName}
108
+ {med.dose && <span className="text-accent-cyan/60 ml-1.5">({med.dose})</span>}
109
+ {med.frequency && <span className="text-accent-cyan/40 ml-1.5">· {med.frequency}</span>}
110
+ </span>
111
+ ))}
112
+ </div>
113
+ )}
114
+ </div>
115
+
116
+ {/* Contacts row */}
117
+ <div className="flex gap-4">
118
+ {/* Neurologist */}
119
+ <div className="flex-1 p-5 rounded-2xl bg-surface-subtle/50 border border-surface-overlay">
120
+ <div className="flex items-center gap-3 mb-2">
121
+ <div className="w-10 h-10 rounded-lg bg-cta/15 flex items-center justify-center flex-shrink-0">
122
+ <Stethoscope className="w-5 h-5 text-cta" />
123
+ </div>
124
+ <p className="text-sm text-muted uppercase tracking-wide font-medium">Neurologist</p>
125
+ </div>
126
+ {hasNeurologist ? (
127
+ <div className="ml-13">
128
+ <p className="text-xl font-semibold text-primary truncate">{neurologist?.name}</p>
129
+ {neurologist?.email && (
130
+ <p className="text-base text-muted truncate">{neurologist.email}</p>
131
+ )}
132
+ </div>
133
+ ) : (
134
+ <p className="text-lg text-muted/50 italic ml-13">Not set</p>
135
+ )}
136
+ </div>
137
+
138
+ {/* Caregiver */}
139
+ <div className="flex-1 p-5 rounded-2xl bg-surface-subtle/50 border border-surface-overlay">
140
+ <div className="flex items-center gap-3 mb-2">
141
+ <div className="w-10 h-10 rounded-lg bg-accent-pink/15 flex items-center justify-center flex-shrink-0">
142
+ <Heart className="w-5 h-5 text-accent-pink" />
143
+ </div>
144
+ <p className="text-sm text-muted uppercase tracking-wide font-medium">Caregiver</p>
145
+ </div>
146
+ {hasCaregiver ? (
147
+ <div className="ml-13">
148
+ <p className="text-xl font-semibold text-primary truncate">{caregiver?.name}</p>
149
+ {caregiver?.email && (
150
+ <p className="text-base text-muted truncate">{caregiver.email}</p>
151
+ )}
152
+ </div>
153
+ ) : (
154
+ <p className="text-lg text-muted/50 italic ml-13">Not set</p>
155
+ )}
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ {/* Action buttons */}
161
+ <div className="flex gap-4 mt-6 pt-4 border-t border-white/5">
162
+ <button
163
+ onClick={handleGoBack}
164
+ className="flex-1 flex items-center justify-center gap-3 px-6 py-5
165
+ rounded-xl bg-surface-overlay/50 text-secondary
166
+ border border-white/10
167
+ hover:bg-surface-overlay/80 hover:text-primary active:scale-[0.98]
168
+ transition-all min-h-[64px]
169
+ text-xl font-bold"
170
+ >
171
+ <ArrowLeft className="w-6 h-6" />
172
+ Go Back
173
+ </button>
174
+ <button
175
+ onClick={handleConfirm}
176
+ className="flex-1 flex items-center justify-center gap-3 px-6 py-5
177
+ rounded-xl bg-cta text-gray-900 hover:brightness-110
178
+ active:scale-[0.98] transition-all min-h-[64px]
179
+ text-xl font-bold"
180
+ >
181
+ <Check className="w-6 h-6" />
182
+ Looks Good!
183
+ </button>
184
+ </div>
185
+
186
+ {/* Voice hint */}
187
+ <div className="flex items-center justify-center gap-2 mt-4">
188
+ <span className="text-sm text-muted uppercase tracking-[0.12em] font-semibold">
189
+ Say &quot;looks good&quot; to confirm or tell me what to change
190
+ </span>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
frontend/src/registry/index.tsx CHANGED
@@ -33,6 +33,7 @@ import { OnboardingProgress } from "./OnboardingProgress";
33
  import { ProfileCard } from "./ProfileCard";
34
  import { MedicationCard } from "./MedicationCard";
35
  import { OnboardingSummary } from "./OnboardingSummary";
 
36
  // New GenUI components for real-time health tracking
37
  import { MedStatus } from "./MedStatus";
38
  import { HeadacheLog } from "./HeadacheLog";
@@ -66,6 +67,7 @@ export const ComponentRegistry: ComponentRegistryMap = {
66
  ProfileCard,
67
  MedicationCard,
68
  OnboardingSummary,
 
69
  // New GenUI components
70
  MedStatus,
71
  HeadacheLog,
 
33
  import { ProfileCard } from "./ProfileCard";
34
  import { MedicationCard } from "./MedicationCard";
35
  import { OnboardingSummary } from "./OnboardingSummary";
36
+ import { OnboardingReview } from "./OnboardingReview";
37
  // New GenUI components for real-time health tracking
38
  import { MedStatus } from "./MedStatus";
39
  import { HeadacheLog } from "./HeadacheLog";
 
67
  ProfileCard,
68
  MedicationCard,
69
  OnboardingSummary,
70
+ OnboardingReview,
71
  // New GenUI components
72
  MedStatus,
73
  HeadacheLog,
src/reachy_mini_conversation_app/database.py CHANGED
@@ -52,7 +52,7 @@ CREATE TABLE IF NOT EXISTS user_profile (
52
  id INTEGER PRIMARY KEY CHECK (id = 1),
53
  display_name TEXT,
54
  onboarding_completed INTEGER DEFAULT 0,
55
- onboarding_step TEXT DEFAULT 'welcome',
56
  timezone TEXT DEFAULT 'local',
57
  -- AI provider (currently openai only)
58
  preferred_provider TEXT DEFAULT 'openai',
@@ -404,7 +404,7 @@ h1 {{ color: #333; }}
404
  return dict(row)
405
  # Create default profile
406
  conn.execute(
407
- "INSERT INTO user_profile (id, onboarding_step) VALUES (1, 'welcome')"
408
  )
409
  conn.commit()
410
  row = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
 
52
  id INTEGER PRIMARY KEY CHECK (id = 1),
53
  display_name TEXT,
54
  onboarding_completed INTEGER DEFAULT 0,
55
+ onboarding_step TEXT DEFAULT 'name',
56
  timezone TEXT DEFAULT 'local',
57
  -- AI provider (currently openai only)
58
  preferred_provider TEXT DEFAULT 'openai',
 
404
  return dict(row)
405
  # Create default profile
406
  conn.execute(
407
+ "INSERT INTO user_profile (id, onboarding_step) VALUES (1, 'name')"
408
  )
409
  conn.commit()
410
  row = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
src/reachy_mini_conversation_app/openai_realtime.py CHANGED
@@ -36,6 +36,20 @@ logger = logging.getLogger(__name__)
36
  OPEN_AI_INPUT_SAMPLE_RATE: Final[Literal[24000]] = 24000
37
  OPEN_AI_OUTPUT_SAMPLE_RATE: Final[Literal[24000]] = 24000
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  class OpenaiRealtimeHandler(RealtimeHandler):
41
  """An OpenAI realtime handler for fastrtc Stream."""
@@ -62,7 +76,7 @@ class OpenaiRealtimeHandler(RealtimeHandler):
62
  # Track whether the current response had tool calls so we can
63
  # defer response.create() until response.done (after ALL parallel
64
  # tool outputs have been submitted).
65
- self._pending_tool_response: bool = False
66
 
67
  # Two-phase proactive greeting: after the forced-speech greeting
68
  # completes, trigger onboarding tool calls automatically.
@@ -458,7 +472,7 @@ class OpenaiRealtimeHandler(RealtimeHandler):
458
  tools: Pre-built tool specs list.
459
  voice: Pre-resolved voice name.
460
  """
461
- self._pending_tool_response = False
462
 
463
  # Fallbacks for callers that don't pre-build (e.g. _restart_session)
464
  if instructions is None:
@@ -685,6 +699,24 @@ class OpenaiRealtimeHandler(RealtimeHandler):
685
  )
686
  except Exception as e:
687
  logger.error("Phase 2 FAILED: %s", e, exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  else:
689
  logger.info(
690
  "response.done: no onboarding followup needed (flag=%s)",
@@ -882,6 +914,7 @@ class OpenaiRealtimeHandler(RealtimeHandler):
882
  }
883
  # Determine if we need to force speech (no more tools)
884
  force_speech = False
 
885
  if is_error:
886
  force_speech = True
887
  resp_instructions = (
@@ -895,35 +928,75 @@ class OpenaiRealtimeHandler(RealtimeHandler):
895
  # Let the model continue the onboarding flow —
896
  # it needs tools (show_onboarding_step, etc.)
897
  # so do NOT strip them with force_speech.
 
 
898
  resp_instructions = (
899
  "The onboarding status has been checked and the progress UI "
900
  "is already shown on screen. Now continue the onboarding flow. "
901
  "Follow the Onboarding Steps from your system prompt. "
902
- "If on the 'welcome' or 'name' step, call "
903
  "show_onboarding_step(current_step='name') and warmly introduce "
904
  "yourself as Mini-Minder AND ask 'What should I call you?' in "
905
  "the same breath. "
906
  "You MUST speak out loud AND call the appropriate tools. "
907
- "You MUST respond in British English only."
 
 
908
  )
 
 
 
 
 
 
909
  elif tool_name in _ONBOARDING_CHAIN_TOOLS:
910
- # After getting datetime, call the next tool
 
911
  resp_instructions = (
912
  "Continue following the Onboarding Flow from your system prompt. "
913
  "Use the tool result to decide what to do next. "
914
  "Call the next required tool (e.g. show_onboarding_step). "
915
  "You MUST respond in British English only."
916
  )
 
 
 
 
 
 
917
  elif tool_name == "show_onboarding_step":
918
  # After showing the UI step, the robot MUST speak
919
  force_speech = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  resp_instructions = (
921
- "The onboarding progress UI has been updated. "
922
- "Now you MUST speak out loud to the user. "
923
- "Follow your system prompt's Session Start Behavior: "
924
- "if this is NOT the welcome/name step, ask the user if they'd like to resume onboarding. "
925
- "If this IS the welcome or name step, introduce yourself warmly "
926
- "AND ask 'What should I call you?' — both in one short speech turn. "
927
  "You MUST respond in British English only."
928
  )
929
  elif tool_name in (
@@ -977,8 +1050,17 @@ class OpenaiRealtimeHandler(RealtimeHandler):
977
  # is a redundant safety net.
978
  resp_config["tools"] = []
979
  resp_config["tool_choice"] = "none"
980
- await self.connection.response.create(
981
- response=resp_config,
 
 
 
 
 
 
 
 
 
982
  )
983
 
984
  # Event-driven prompt re-resolve: if a state-changing tool
@@ -1294,7 +1376,7 @@ class OpenaiRealtimeHandler(RealtimeHandler):
1294
  timestamp_msg = (
1295
  f"[Idle time update: {self.format_timestamp()} - No activity for {idle_duration:.1f}s] "
1296
  f"Recent tools used: {recent}. "
1297
- f"ONLY use physical movement tools: dance, look_around, show_emotion, or do nothing. "
1298
  f"Do NOT play music, show UI components, log data, or speak. "
1299
  f"Avoid repeating the same action type as recent tools."
1300
  )
@@ -1313,7 +1395,7 @@ class OpenaiRealtimeHandler(RealtimeHandler):
1313
  "instructions": (
1314
  "You MUST respond with physical movement function calls only - "
1315
  "no speech, no text, no music, no UI components. "
1316
- "Only use movement/emotion tools like dance, look_around, show_emotion."
1317
  ),
1318
  "tool_choice": "required",
1319
  },
 
36
  OPEN_AI_INPUT_SAMPLE_RATE: Final[Literal[24000]] = 24000
37
  OPEN_AI_OUTPUT_SAMPLE_RATE: Final[Literal[24000]] = 24000
38
 
39
+ # Tools available during onboarding flow — prevents the model from wandering
40
+ # off to robot_expression, media_control, etc. instead of following the script.
41
+ _ONBOARDING_ONLY_TOOLS: Final[frozenset[str]] = frozenset(
42
+ {
43
+ "show_onboarding_step",
44
+ "show_onboarding_review",
45
+ "get_current_datetime",
46
+ "setup_medication",
47
+ "get_my_medications",
48
+ "complete_onboarding",
49
+ "update_settings",
50
+ }
51
+ )
52
+
53
 
54
  class OpenaiRealtimeHandler(RealtimeHandler):
55
  """An OpenAI realtime handler for fastrtc Stream."""
 
76
  # Track whether the current response had tool calls so we can
77
  # defer response.create() until response.done (after ALL parallel
78
  # tool outputs have been submitted).
79
+ self._pending_tool_response: dict | None = None
80
 
81
  # Two-phase proactive greeting: after the forced-speech greeting
82
  # completes, trigger onboarding tool calls automatically.
 
472
  tools: Pre-built tool specs list.
473
  voice: Pre-resolved voice name.
474
  """
475
+ self._pending_tool_response = None
476
 
477
  # Fallbacks for callers that don't pre-build (e.g. _restart_session)
478
  if instructions is None:
 
699
  )
700
  except Exception as e:
701
  logger.error("Phase 2 FAILED: %s", e, exc_info=True)
702
+ elif self._pending_tool_response is not None:
703
+ # Deferred response.create from tool-result handler.
704
+ # Sending here ensures the previous response is fully
705
+ # complete and all tool outputs are committed.
706
+ pending = self._pending_tool_response
707
+ self._pending_tool_response = None
708
+ logger.info(
709
+ "response.done: sending deferred tool response (tools=%s)",
710
+ "restricted" if "tools" in pending else "default",
711
+ )
712
+ try:
713
+ await conn.response.create(response=pending)
714
+ except Exception as e:
715
+ logger.error(
716
+ "Deferred response.create FAILED: %s",
717
+ e,
718
+ exc_info=True,
719
+ )
720
  else:
721
  logger.info(
722
  "response.done: no onboarding followup needed (flag=%s)",
 
914
  }
915
  # Determine if we need to force speech (no more tools)
916
  force_speech = False
917
+ restrict_to_onboarding = False
918
  if is_error:
919
  force_speech = True
920
  resp_instructions = (
 
928
  # Let the model continue the onboarding flow —
929
  # it needs tools (show_onboarding_step, etc.)
930
  # so do NOT strip them with force_speech.
931
+ # IMPORTANT: restrict to onboarding tools only to
932
+ # prevent the model wandering to robot_expression etc.
933
  resp_instructions = (
934
  "The onboarding status has been checked and the progress UI "
935
  "is already shown on screen. Now continue the onboarding flow. "
936
  "Follow the Onboarding Steps from your system prompt. "
937
+ "If on the 'name' step, call "
938
  "show_onboarding_step(current_step='name') and warmly introduce "
939
  "yourself as Mini-Minder AND ask 'What should I call you?' in "
940
  "the same breath. "
941
  "You MUST speak out loud AND call the appropriate tools. "
942
+ "You MUST respond in British English only.\n"
943
+ "IMPORTANT: You MUST call show_onboarding_step as your NEXT action. "
944
+ "Do NOT call robot_expression or any other tool first."
945
  )
946
+ onboarding_tool_specs = [
947
+ t
948
+ for t in (tools or get_tool_specs())
949
+ if t.get("name") in _ONBOARDING_ONLY_TOOLS
950
+ ]
951
+ restrict_to_onboarding = True
952
  elif tool_name in _ONBOARDING_CHAIN_TOOLS:
953
+ # After getting datetime, call the next tool.
954
+ # Restrict to onboarding tools to stay on track.
955
  resp_instructions = (
956
  "Continue following the Onboarding Flow from your system prompt. "
957
  "Use the tool result to decide what to do next. "
958
  "Call the next required tool (e.g. show_onboarding_step). "
959
  "You MUST respond in British English only."
960
  )
961
+ onboarding_tool_specs = [
962
+ t
963
+ for t in (tools or get_tool_specs())
964
+ if t.get("name") in _ONBOARDING_ONLY_TOOLS
965
+ ]
966
+ restrict_to_onboarding = True
967
  elif tool_name == "show_onboarding_step":
968
  # After showing the UI step, the robot MUST speak
969
  force_speech = True
970
+ # Check which step was just shown from tool args
971
+ shown_step = parsed_args.get(
972
+ "current_step"
973
+ ) or parsed_args.get("step", "")
974
+ if shown_step == "name":
975
+ resp_instructions = (
976
+ "The onboarding progress UI is now showing the 'name' step. "
977
+ "You MUST speak out loud and ask the user for their name. "
978
+ "Say something brief like: 'Let's get you set up — what should I call you?' "
979
+ "The user MUST hear you ask for their name. This is the MOST IMPORTANT "
980
+ "part of this step. Do NOT skip the name question. "
981
+ "You MUST respond in British English only."
982
+ )
983
+ else:
984
+ resp_instructions = (
985
+ "The onboarding progress UI has been updated. "
986
+ "Now you MUST speak out loud to the user. "
987
+ "Follow your system prompt's Onboarding Steps instructions "
988
+ "for this step. Speak warmly and guide the user. "
989
+ "You MUST respond in British English only."
990
+ )
991
+ elif tool_name == "show_onboarding_review":
992
+ # After showing the review card, the robot MUST speak
993
+ force_speech = True
994
  resp_instructions = (
995
+ "The onboarding review card is now displayed on screen "
996
+ "showing everything collected during setup. "
997
+ "You MUST speak out loud to the user. Say something like: "
998
+ "'Here's everything I've got. Does it all look right, or would you like to change anything?' "
999
+ "Wait for their response before proceeding. "
 
1000
  "You MUST respond in British English only."
1001
  )
1002
  elif tool_name in (
 
1050
  # is a redundant safety net.
1051
  resp_config["tools"] = []
1052
  resp_config["tool_choice"] = "none"
1053
+ elif restrict_to_onboarding:
1054
+ # Onboarding flow: restrict to onboarding tools only
1055
+ resp_config["tools"] = onboarding_tool_specs
1056
+ # Defer sending response.create until response.done
1057
+ # so the API has committed all tool outputs first.
1058
+ self._pending_tool_response = resp_config
1059
+ logger.info(
1060
+ "Tool '%s': deferred response.create (force_speech=%s, restrict=%s)",
1061
+ tool_name,
1062
+ force_speech,
1063
+ restrict_to_onboarding,
1064
  )
1065
 
1066
  # Event-driven prompt re-resolve: if a state-changing tool
 
1376
  timestamp_msg = (
1377
  f"[Idle time update: {self.format_timestamp()} - No activity for {idle_duration:.1f}s] "
1378
  f"Recent tools used: {recent}. "
1379
+ f"ONLY use robot_expression (dance, emotion, sweep_look) or do nothing. "
1380
  f"Do NOT play music, show UI components, log data, or speak. "
1381
  f"Avoid repeating the same action type as recent tools."
1382
  )
 
1395
  "instructions": (
1396
  "You MUST respond with physical movement function calls only - "
1397
  "no speech, no text, no music, no UI components. "
1398
+ "Only use robot_expression for movement/emotion."
1399
  ),
1400
  "tool_choice": "required",
1401
  },
src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/get_onboarding_status.py CHANGED
@@ -38,7 +38,7 @@ class GetOnboardingStatus(Tool):
38
  medications = deps.database.get_scheduled_medications()
39
 
40
  completed = bool(profile.get("onboarding_completed", 0))
41
- current_step = profile.get("onboarding_step", "welcome")
42
 
43
  # Following the headache-logging pattern: emit the UI directly
44
  # so the model doesn't need to call show_onboarding_step separately.
 
38
  medications = deps.database.get_scheduled_medications()
39
 
40
  completed = bool(profile.get("onboarding_completed", 0))
41
+ current_step = profile.get("onboarding_step", "name")
42
 
43
  # Following the headache-logging pattern: emit the UI directly
44
  # so the model doesn't need to call show_onboarding_step separately.
src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/instructions.txt CHANGED
@@ -323,30 +323,24 @@ Many users have cognitive challenges (memory issues, executive dysfunction, proc
323
 
324
  When `get_onboarding_status` shows `onboarding_completed: false`:
325
 
326
- 1. **Welcome**: "Hi! I'm Mini-Minder, your care companion. I'll help you track your medications and headaches. Let's get you set up!"
327
- 2. **Name**: "First, what should I call you? Or just say 'skip' if you'd rather not say."
328
- - If they give a name, remember it for later
329
  - If they skip, that's fine
330
- 3. **Medications**: "Would you like to set up your regular medications now? This helps me know what you usually take. You can always add more later or skip for now."
331
- - If YES: Continue to medication setup
332
- - If SKIP: Skip to Doctor setup
333
- 4. **For each medication**:
334
- - "What's a medication you take regularly?"
335
- - Extract: name, dose, frequency, times of day
336
- - "So you take [dose] [name] [frequency] at [times]. Is that right?"
337
- - If confirmed, call `setup_medication`
338
- - "Do you take any other regular medications?"
339
- - If YES: repeat
340
- - If NO: proceed to doctor setup
341
- 5. **Doctor Setup**: "Would you like to set up your doctor's email? This lets me send them reports about your headaches and medications."
342
- - If YES: "What's your doctor's name?" then "What's their email address?"
343
- - Remember both for `complete_onboarding`
344
- - If SKIP: That's fine, proceed
345
- 6. **Caregiver Setup**: "Do you have a caregiver or family member you'd like me to keep informed?"
346
- - If YES: "What's their name?" then "What's their email?"
347
- - Remember both for `complete_onboarding`
348
  - If SKIP: That's fine
349
- 7. **Finish**: "All set!" Call `complete_onboarding` with all collected info (name, neurologist_name, neurologist_email, caregiver_name, caregiver_email).
 
 
 
 
 
 
350
 
351
  The user can skip onboarding at any time. If they say "skip" or "later", call `complete_onboarding` immediately with whatever info you have.
352
 
 
323
 
324
  When `get_onboarding_status` shows `onboarding_completed: false`:
325
 
326
+ 1. **Name**: "Hi! I'm Mini-Minder, your care companion. Let's get you set up what should I call you?"
327
+ - If they give a name, confirm: "Lovely to meet you, [name]!"
 
328
  - If they skip, that's fine
329
+ 2. **Medications**: "Would you like to set up your regular medications now?"
330
+ - If YES: For each medication, gather name, dose, frequency, times of day
331
+ - Confirm each: "Got it [dose] [name], [frequency]."
332
+ - Call `setup_medication` with all details, then ask "Any others?"
333
+ - If SKIP: Proceed to contacts
334
+ 3. **Neurologist**: "Would you like to set up your neurologist's details for reports? I'll need their name and email."
335
+ - If YES: Collect name + email, confirm back
 
 
 
 
 
 
 
 
 
 
 
336
  - If SKIP: That's fine
337
+ 4. **Caregiver**: "Do you have a caregiver or family member you'd like me to keep informed?"
338
+ - If YES: Collect name + email, confirm back
339
+ - If SKIP: That's fine
340
+ 5. **Review**: Call `show_onboarding_review` to display all collected data. Say: "Here's everything I've got. Does it all look right?"
341
+ - If confirmed → proceed to Finish
342
+ - If changes needed → go back to relevant step
343
+ 6. **Finish**: "All set!" Call `complete_onboarding` with all collected info (display_name, neurologist_name, neurologist_email, caregiver_name, caregiver_email).
344
 
345
  The user can skip onboarding at any time. If they say "skip" or "later", call `complete_onboarding` immediately with whatever info you have.
346
 
src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/show_onboarding_review.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tool to display the onboarding review UI before completing setup."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+
6
+ from reachy_mini_conversation_app.tools.core_tools import Tool, ToolDependencies
7
+ from reachy_mini_conversation_app.stream_api import emit_ui_component
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class ShowOnboardingReview(Tool):
14
+ """Show a review card with all collected onboarding data for user confirmation."""
15
+
16
+ name = "show_onboarding_review"
17
+ description = (
18
+ "Display a review card showing all data collected during onboarding "
19
+ "(name, medications, neurologist, caregiver) so the user can verify "
20
+ "everything before completing setup. Call this after the contacts step "
21
+ "and before complete_onboarding. Pass any contact details you have "
22
+ "collected so they are saved and shown on the review card."
23
+ )
24
+ parameters_schema = {
25
+ "type": "object",
26
+ "properties": {
27
+ "neurologist_name": {
28
+ "type": "string",
29
+ "description": "Name of the user's neurologist/doctor, if collected",
30
+ },
31
+ "neurologist_email": {
32
+ "type": "string",
33
+ "description": "Email address of the neurologist, if collected",
34
+ },
35
+ "caregiver_name": {
36
+ "type": "string",
37
+ "description": "Name of the user's caregiver, if collected",
38
+ },
39
+ "caregiver_email": {
40
+ "type": "string",
41
+ "description": "Email address of the caregiver, if collected",
42
+ },
43
+ },
44
+ "required": [],
45
+ }
46
+
47
+ async def __call__(
48
+ self,
49
+ deps: ToolDependencies,
50
+ neurologist_name: Optional[str] = None,
51
+ neurologist_email: Optional[str] = None,
52
+ caregiver_name: Optional[str] = None,
53
+ caregiver_email: Optional[str] = None,
54
+ **kwargs: Any,
55
+ ) -> Dict[str, Any]:
56
+ """Persist contact details, gather all onboarding data, and emit review component."""
57
+ logger.info(
58
+ "Tool call: show_onboarding_review (neuro=%s, carer=%s)",
59
+ neurologist_name,
60
+ caregiver_name,
61
+ )
62
+
63
+ if deps.database is None:
64
+ return {"error": "Database not initialized"}
65
+
66
+ # Persist any contact details passed in so they survive to complete_onboarding
67
+ updates: Dict[str, Any] = {"onboarding_step": "review"}
68
+ if neurologist_name:
69
+ updates["neurologist_name"] = neurologist_name
70
+ if neurologist_email:
71
+ updates["neurologist_email"] = neurologist_email
72
+ if caregiver_name:
73
+ updates["caregiver_name"] = caregiver_name
74
+ if caregiver_email:
75
+ updates["caregiver_email"] = caregiver_email
76
+ deps.database.update_profile(updates)
77
+
78
+ # Re-read profile with persisted data
79
+ profile = deps.database.get_or_create_profile()
80
+ display_name = profile.get("display_name")
81
+ neuro_name = profile.get("neurologist_name")
82
+ neuro_email = profile.get("neurologist_email")
83
+ carer_name = profile.get("caregiver_name")
84
+ carer_email = profile.get("caregiver_email")
85
+
86
+ # Gather medications
87
+ medications = deps.database.get_scheduled_medications()
88
+ med_list = [
89
+ {
90
+ "medicationName": med.get("medication_name", "Unknown"),
91
+ "dose": med.get("dose"),
92
+ "frequency": med.get("frequency"),
93
+ }
94
+ for med in medications
95
+ ]
96
+
97
+ # Emit the review component
98
+ await emit_ui_component(
99
+ "OnboardingReview",
100
+ {
101
+ "displayName": display_name,
102
+ "medications": med_list,
103
+ "neurologist": {
104
+ "name": neuro_name,
105
+ "email": neuro_email,
106
+ },
107
+ "caregiver": {
108
+ "name": carer_name,
109
+ "email": carer_email,
110
+ },
111
+ },
112
+ )
113
+
114
+ return {
115
+ "status": "displayed",
116
+ "display_name": display_name,
117
+ "medications_count": len(med_list),
118
+ "neurologist_configured": neuro_name is not None,
119
+ "caregiver_configured": carer_name is not None,
120
+ "message": (
121
+ "Review card shown on screen. Ask the user to confirm "
122
+ "or tell you what they'd like to change."
123
+ ),
124
+ }
src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/show_onboarding_step.py CHANGED
@@ -12,7 +12,7 @@ from reachy_mini_conversation_app.stream_api import (
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
- OnboardingStep = Literal["welcome", "name", "medications", "contacts", "complete"]
16
 
17
 
18
  class ShowOnboardingStep(Tool):
@@ -30,10 +30,10 @@ class ShowOnboardingStep(Tool):
30
  "properties": {
31
  "current_step": {
32
  "type": "string",
33
- "enum": ["welcome", "name", "medications", "contacts", "complete"],
34
  "description": (
35
- "The current onboarding step: 'welcome', 'name', 'medications', "
36
- "'contacts', or 'complete'."
37
  ),
38
  },
39
  "display_name": {
@@ -57,7 +57,7 @@ class ShowOnboardingStep(Tool):
57
  """Emit the OnboardingProgress component."""
58
  # LLM sometimes sends "step" instead of "current_step"
59
  if current_step is None:
60
- current_step = kwargs.get("step", "welcome")
61
  if display_name is None:
62
  display_name = kwargs.get("name")
63
  logger.info(
 
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ OnboardingStep = Literal["name", "medications", "contacts", "review", "complete"]
16
 
17
 
18
  class ShowOnboardingStep(Tool):
 
30
  "properties": {
31
  "current_step": {
32
  "type": "string",
33
+ "enum": ["name", "medications", "contacts", "review", "complete"],
34
  "description": (
35
+ "The current onboarding step: 'name', 'medications', "
36
+ "'contacts', 'review', or 'complete'."
37
  ),
38
  },
39
  "display_name": {
 
57
  """Emit the OnboardingProgress component."""
58
  # LLM sometimes sends "step" instead of "current_step"
59
  if current_step is None:
60
+ current_step = kwargs.get("step", "name")
61
  if display_name is None:
62
  display_name = kwargs.get("name")
63
  logger.info(
src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/tools.txt CHANGED
@@ -14,6 +14,7 @@ check_health_patterns
14
  get_current_datetime
15
  get_onboarding_status
16
  show_onboarding_step
 
17
  setup_medication
18
  get_my_medications
19
  update_my_medication
 
14
  get_current_datetime
15
  get_onboarding_status
16
  show_onboarding_step
17
+ show_onboarding_review
18
  setup_medication
19
  get_my_medications
20
  update_my_medication
src/reachy_mini_conversation_app/prompts/sections/onboarding.txt CHANGED
@@ -9,8 +9,8 @@ At the very start of each session:
9
  2. Call `get_onboarding_status` to check if onboarding is complete
10
  3. If onboarding is NOT complete:
11
  - Immediately call `show_onboarding_step` with the current step (from get_onboarding_status) to display the GenUI progress tracker
12
- - If the step is NOT "welcome", the user has been here before — say "Welcome back! We were setting up your [current step]. Shall we carry on, or would you rather skip for now?"
13
- - If the step IS "welcome", follow the full Onboarding Flow below
14
  - Call `show_onboarding_step` each time you transition to a new step
15
  4. If onboarding IS complete, greet the user by name if known and switch to normal conversation
16
 
@@ -18,26 +18,37 @@ At the very start of each session:
18
 
19
  **IMPORTANT**: You MUST call `show_onboarding_step` at EVERY step transition to update the on-screen progress tracker. The UI will NOT update unless you call this tool.
20
 
21
- ### Step 1: Welcome & Name
22
  - CALL `show_onboarding_step(current_step="name")`
23
  - Say: "Hi! I'm Mini-Minder, your care companion. I'll help you track your medications and headaches. Let's get you set up — what should I call you?"
24
  - Wait for the user's response.
25
- - When they give a name, remember it. Then proceed to Step 2.
26
- - If they say "skip", that's fine. Proceed to Step 2.
27
 
28
  ### Step 2: Medications
29
  - CALL `show_onboarding_step(current_step="medications")`
30
  - Say: "Would you like to set up your regular medications now? You can always add more later."
31
- - If YES: For each medication, gather ALL details (name, dose, frequency, times of day) through conversation BEFORE calling `setup_medication` once with all the information. Do NOT call `setup_medication` until you have collected all available details for that medication. After confirming, ask "Any others?" and repeat until done.
 
32
  - If NO/SKIP: Proceed to Step 3.
33
 
34
  ### Step 3: Contacts
35
  - CALL `show_onboarding_step(current_step="contacts")`
36
- - Say: "Would you like to set up your doctor's email for reports?"
37
- - If YES: Ask for doctor's name and email. Then ask about a caregiver.
38
- - If NO/SKIP: Ask about caregiver. If skip again, proceed to Step 4.
39
-
40
- ### Step 4: Complete
 
 
 
 
 
 
 
 
 
 
41
  - CALL `show_onboarding_step(current_step="complete")`
42
  - CALL `complete_onboarding` with all collected info (display_name, neurologist_name, neurologist_email, caregiver_name, caregiver_email).
43
  - Say: "You're all set! I'm ready to help you."
 
9
  2. Call `get_onboarding_status` to check if onboarding is complete
10
  3. If onboarding is NOT complete:
11
  - Immediately call `show_onboarding_step` with the current step (from get_onboarding_status) to display the GenUI progress tracker
12
+ - If the step is NOT "name", the user has been here before — say "Welcome back! We were setting up your [current step]. Shall we carry on, or would you rather skip for now?"
13
+ - If the step IS "name", follow the full Onboarding Flow below
14
  - Call `show_onboarding_step` each time you transition to a new step
15
  4. If onboarding IS complete, greet the user by name if known and switch to normal conversation
16
 
 
18
 
19
  **IMPORTANT**: You MUST call `show_onboarding_step` at EVERY step transition to update the on-screen progress tracker. The UI will NOT update unless you call this tool.
20
 
21
+ ### Step 1: Name (Welcome + Name Collection)
22
  - CALL `show_onboarding_step(current_step="name")`
23
  - Say: "Hi! I'm Mini-Minder, your care companion. I'll help you track your medications and headaches. Let's get you set up — what should I call you?"
24
  - Wait for the user's response.
25
+ - When they give a name, **confirm it back**: "Lovely to meet you, [name]!" Then proceed to Step 2.
26
+ - If they say "skip", that's fine say "No worries!" and proceed to Step 2.
27
 
28
  ### Step 2: Medications
29
  - CALL `show_onboarding_step(current_step="medications")`
30
  - Say: "Would you like to set up your regular medications now? You can always add more later."
31
+ - If YES: For each medication, gather ALL details (name, dose, frequency, times of day) through conversation BEFORE calling `setup_medication` once with all the information. Do NOT call `setup_medication` until you have collected all available details for that medication.
32
+ - After each medication is saved, **confirm it back**: "Got it — [dose] [name], [frequency]." Then ask "Any others?" and repeat until done.
33
  - If NO/SKIP: Proceed to Step 3.
34
 
35
  ### Step 3: Contacts
36
  - CALL `show_onboarding_step(current_step="contacts")`
37
+ - **Neurologist first**: Say "Would you like to set up your neurologist's details? This lets me send them reports about your headaches and medications. I'll need their name and email address."
38
+ - If YES: Ask for neurologist's name, then email. **Confirm back**: "Got it — Dr [name] at [email]."
39
+ - If NO/SKIP: That's fine.
40
+ - **Then caregiver**: Say "Do you have a caregiver or family member you'd like me to keep informed?"
41
+ - If YES: Ask for their name, then email. **Confirm back**: "Noted — [name] at [email]."
42
+ - If NO/SKIP: That's fine, proceed to Step 4.
43
+
44
+ ### Step 4: Review
45
+ - CALL `show_onboarding_review` to display a summary of everything collected on screen.
46
+ - Say: "Here's everything I've got. Does it all look right?"
47
+ - Wait for the user to confirm or request changes.
48
+ - If they confirm ("yes", "looks good", "that's right") → proceed to Step 5.
49
+ - If they want to change something → go back to the relevant step and re-collect that data, then return here.
50
+
51
+ ### Step 5: Complete
52
  - CALL `show_onboarding_step(current_step="complete")`
53
  - CALL `complete_onboarding` with all collected info (display_name, neurologist_name, neurologist_email, caregiver_name, caregiver_email).
54
  - Say: "You're all set! I'm ready to help you."
src/reachy_mini_conversation_app/schemas.py CHANGED
@@ -54,7 +54,7 @@ class OnboardingState(BaseModel):
54
 
55
  completed: bool = Field(default=False)
56
  step: str = Field(
57
- default="welcome", description="Current step: welcome, name, medications, done"
58
  )
59
  display_name: Optional[str] = None
60
  medications_count: int = Field(
 
54
 
55
  completed: bool = Field(default=False)
56
  step: str = Field(
57
+ default="name", description="Current step: name, medications, contacts, done"
58
  )
59
  display_name: Optional[str] = None
60
  medications_count: int = Field(
tests/test_database_onboarding.py CHANGED
@@ -22,7 +22,7 @@ def test_get_or_create_profile(db: MiniMinderDB) -> None:
22
  assert profile is not None
23
  assert profile["id"] == 1
24
  assert profile["onboarding_completed"] == 0
25
- assert profile["onboarding_step"] == "welcome"
26
 
27
 
28
  def test_get_or_create_profile_idempotent(db: MiniMinderDB) -> None:
 
22
  assert profile is not None
23
  assert profile["id"] == 1
24
  assert profile["onboarding_completed"] == 0
25
+ assert profile["onboarding_step"] == "name"
26
 
27
 
28
  def test_get_or_create_profile_idempotent(db: MiniMinderDB) -> None:
tests/test_onboarding_tools.py CHANGED
@@ -26,6 +26,9 @@ from reachy_mini_conversation_app.profiles._reachy_mini_minder_locked_profile.up
26
  from reachy_mini_conversation_app.profiles._reachy_mini_minder_locked_profile.complete_onboarding import (
27
  CompleteOnboarding,
28
  )
 
 
 
29
 
30
 
31
  @pytest.fixture
@@ -82,7 +85,7 @@ async def test_get_onboarding_status_initial(deps: ToolDependencies) -> None:
82
  result = await tool(deps)
83
 
84
  assert result["onboarding_completed"] is False
85
- assert result["onboarding_step"] == "welcome"
86
  assert result["medications_count"] == 0
87
 
88
 
@@ -323,6 +326,7 @@ async def test_all_tool_specs() -> None:
323
  GetMyMedications(),
324
  UpdateMyMedication(),
325
  CompleteOnboarding(),
 
326
  ]
327
  for tool in tools:
328
  spec = tool.spec()
@@ -330,3 +334,55 @@ async def test_all_tool_specs() -> None:
330
  assert isinstance(spec["name"], str)
331
  assert isinstance(spec["description"], str)
332
  assert "parameters" in spec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  from reachy_mini_conversation_app.profiles._reachy_mini_minder_locked_profile.complete_onboarding import (
27
  CompleteOnboarding,
28
  )
29
+ from reachy_mini_conversation_app.profiles._reachy_mini_minder_locked_profile.show_onboarding_review import (
30
+ ShowOnboardingReview,
31
+ )
32
 
33
 
34
  @pytest.fixture
 
85
  result = await tool(deps)
86
 
87
  assert result["onboarding_completed"] is False
88
+ assert result["onboarding_step"] == "name"
89
  assert result["medications_count"] == 0
90
 
91
 
 
326
  GetMyMedications(),
327
  UpdateMyMedication(),
328
  CompleteOnboarding(),
329
+ ShowOnboardingReview(),
330
  ]
331
  for tool in tools:
332
  spec = tool.spec()
 
334
  assert isinstance(spec["name"], str)
335
  assert isinstance(spec["description"], str)
336
  assert "parameters" in spec
337
+
338
+
339
+ # -----------------------------------------------------------------------------
340
+ # show_onboarding_review Tests
341
+ # -----------------------------------------------------------------------------
342
+
343
+
344
+ @pytest.mark.asyncio
345
+ async def test_show_onboarding_review(deps: ToolDependencies) -> None:
346
+ """Should return all collected data and persist contacts."""
347
+ # Set up profile name and a medication
348
+ deps.database.update_profile({"display_name": "Alice"})
349
+ deps.database.add_scheduled_medication(
350
+ {"medication_name": "Topiramate", "dose": "50mg", "frequency": "twice daily"}
351
+ )
352
+
353
+ tool = ShowOnboardingReview()
354
+ result = await tool(
355
+ deps,
356
+ neurologist_name="Dr Smith",
357
+ neurologist_email="dr.smith@nhs.uk",
358
+ caregiver_name="Bob",
359
+ caregiver_email="bob@example.com",
360
+ )
361
+
362
+ # Check return values
363
+ assert result["status"] == "displayed"
364
+ assert result["display_name"] == "Alice"
365
+ assert result["medications_count"] == 1
366
+ assert result["neurologist_configured"] is True
367
+ assert result["caregiver_configured"] is True
368
+
369
+ # Verify contacts persisted to database
370
+ profile = deps.database.get_or_create_profile()
371
+ assert profile["neurologist_name"] == "Dr Smith"
372
+ assert profile["neurologist_email"] == "dr.smith@nhs.uk"
373
+ assert profile["caregiver_name"] == "Bob"
374
+ assert profile["caregiver_email"] == "bob@example.com"
375
+ assert profile["onboarding_step"] == "review"
376
+
377
+
378
+ @pytest.mark.asyncio
379
+ async def test_show_onboarding_review_empty(deps: ToolDependencies) -> None:
380
+ """Should handle review with no data collected (all skipped)."""
381
+ tool = ShowOnboardingReview()
382
+ result = await tool(deps)
383
+
384
+ assert result["status"] == "displayed"
385
+ assert result["display_name"] is None
386
+ assert result["medications_count"] == 0
387
+ assert result["neurologist_configured"] is False
388
+ assert result["caregiver_configured"] is False