Spaces:
Running
Running
feat: Introduce an onboarding review step and remove the initial welcome step from the onboarding flow.
Browse files- frontend/src/components/ComponentOverlay.tsx +1 -1
- frontend/src/registry/OnboardingProgress.tsx +3 -3
- frontend/src/registry/OnboardingReview.tsx +194 -0
- frontend/src/registry/index.tsx +2 -0
- src/reachy_mini_conversation_app/database.py +2 -2
- src/reachy_mini_conversation_app/openai_realtime.py +97 -15
- src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/get_onboarding_status.py +1 -1
- src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/instructions.txt +16 -22
- src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/show_onboarding_review.py +124 -0
- src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/show_onboarding_step.py +5 -5
- src/reachy_mini_conversation_app/profiles/_reachy_mini_minder_locked_profile/tools.txt +1 -0
- src/reachy_mini_conversation_app/prompts/sections/onboarding.txt +22 -11
- src/reachy_mini_conversation_app/schemas.py +1 -1
- tests/test_database_onboarding.py +1 -1
- tests/test_onboarding_tools.py +57 -1
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: "
|
| 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 "looks good" 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 '
|
| 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, '
|
| 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:
|
| 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 =
|
| 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 '
|
| 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
|
| 922 |
-
"
|
| 923 |
-
"
|
| 924 |
-
"
|
| 925 |
-
"
|
| 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 |
-
|
| 981 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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", "
|
| 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. **
|
| 327 |
-
|
| 328 |
-
- If they give a name, remember it for later
|
| 329 |
- If they skip, that's fine
|
| 330 |
-
|
| 331 |
-
- If YES:
|
| 332 |
-
-
|
| 333 |
-
|
| 334 |
-
-
|
| 335 |
-
|
| 336 |
-
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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["
|
| 16 |
|
| 17 |
|
| 18 |
class ShowOnboardingStep(Tool):
|
|
@@ -30,10 +30,10 @@ class ShowOnboardingStep(Tool):
|
|
| 30 |
"properties": {
|
| 31 |
"current_step": {
|
| 32 |
"type": "string",
|
| 33 |
-
"enum": ["
|
| 34 |
"description": (
|
| 35 |
-
"The current onboarding step: '
|
| 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", "
|
| 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 "
|
| 13 |
-
- If the step IS "
|
| 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
|
| 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,
|
| 26 |
-
- If they say "skip", that's fine
|
| 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 |
- If NO/SKIP: Proceed to Step 3.
|
| 33 |
|
| 34 |
### Step 3: Contacts
|
| 35 |
- CALL `show_onboarding_step(current_step="contacts")`
|
| 36 |
-
-
|
| 37 |
-
- If YES: Ask for
|
| 38 |
-
- If NO/SKIP:
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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"] == "
|
| 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"] == "
|
| 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
|