From aff08d0efb5262b1620ad99c79a3b979ffa8811e Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Fri, 1 May 2026 19:51:57 +0530 Subject: [PATCH] fix(onboarding): personalize welcome agent greeting with user identity (#1078) --- src/openhuman/agent/agents/welcome/prompt.rs | 39 +++++++++++++++++++- src/openhuman/agent/prompts/mod.rs | 20 ++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/openhuman/agent/agents/welcome/prompt.rs b/src/openhuman/agent/agents/welcome/prompt.rs index 67f795b49..f1c107514 100644 --- a/src/openhuman/agent/agents/welcome/prompt.rs +++ b/src/openhuman/agent/agents/welcome/prompt.rs @@ -8,7 +8,8 @@ //! skill-executor wording stays scoped to `integrations_agent/prompt.rs`). use crate::openhuman::context::prompt::{ - render_tools, render_user_files, render_workspace, ConnectedIntegration, PromptContext, + render_tools, render_user_files, render_user_identity, render_workspace, ConnectedIntegration, + PromptContext, }; use anyhow::Result; use std::fmt::Write; @@ -20,6 +21,12 @@ pub fn build(ctx: &PromptContext<'_>) -> Result { out.push_str(ARCHETYPE.trim_end()); out.push_str("\n\n"); + let user_id = render_user_identity(ctx)?; + if !user_id.trim().is_empty() { + out.push_str(user_id.trim_end()); + out.push_str("\n\n"); + } + let user_files = render_user_files(ctx)?; if !user_files.trim().is_empty() { out.push_str(user_files.trim_end()); @@ -134,6 +141,36 @@ mod tests { assert!(!body.contains("## Connected Integrations")); } + #[test] + fn build_injects_user_identity_when_present() { + use crate::openhuman::context::prompt::UserIdentity; + let mut ctx = ctx_with(&[]); + ctx.user_identity = Some(UserIdentity { + name: Some("Alice".into()), + email: Some("alice@example.com".into()), + id: None, + }); + let body = build(&ctx).unwrap(); + assert!( + body.contains("## User"), + "should contain user identity heading" + ); + assert!(body.contains("Alice"), "should contain the user's name"); + assert!( + body.contains("alice@example.com"), + "should contain the user's email" + ); + } + + #[test] + fn build_omits_user_identity_when_absent() { + let body = build(&ctx_with(&[])).unwrap(); + assert!( + !body.contains("## User"), + "should not contain user identity when None" + ); + } + #[test] fn build_lists_only_connected_integrations() { let integrations = vec![ diff --git a/src/openhuman/agent/prompts/mod.rs b/src/openhuman/agent/prompts/mod.rs index d09893257..e80a82333 100644 --- a/src/openhuman/agent/prompts/mod.rs +++ b/src/openhuman/agent/prompts/mod.rs @@ -535,13 +535,13 @@ impl PromptSection for UserIdentitySection { // failure mode we're trying to suppress (#926). let mut fields = String::new(); if let Some(name) = identity.name.as_deref().filter(|s| !s.trim().is_empty()) { - let _ = writeln!(fields, "- name: {name}"); + let _ = writeln!(fields, "- name: {}", sanitize_identity_field(name)); } if let Some(email) = identity.email.as_deref().filter(|s| !s.trim().is_empty()) { - let _ = writeln!(fields, "- email: {email}"); + let _ = writeln!(fields, "- email: {}", sanitize_identity_field(email)); } if let Some(id) = identity.id.as_deref().filter(|s| !s.trim().is_empty()) { - let _ = writeln!(fields, "- id: {id}"); + let _ = writeln!(fields, "- id: {}", sanitize_identity_field(id)); } if fields.trim().is_empty() { return Ok(String::new()); @@ -557,6 +557,20 @@ impl PromptSection for UserIdentitySection { } } +/// Collapse newlines and runs of whitespace in a user-identity field so +/// it fits on a single markdown bullet without breaking the prompt +/// structure. Values come from `auth_get_me` (server-controlled), but +/// defence-in-depth: a name with embedded newlines could split the +/// `- name:` bullet and reshape the `## User` block. +fn sanitize_identity_field(s: &str) -> String { + s.chars() + .map(|c| if c == '\n' || c == '\r' { ' ' } else { c }) + .collect::() + .split_whitespace() + .collect::>() + .join(" ") +} + // ───────────────────────────────────────────────────────────────────────────── // Section helpers for function-driven prompts // ─────────────────────────────────────────────────────────────────────────────