use super::shared_selection::{ManagedItem, SelectionState};
use crate::core::config::data::{Config, Persona};
impl ManagedItem for Persona {
fn id(&self) -> &str {
&self.id
}
}
pub struct PersonaManager {
shared: SelectionState<Persona>,
}
impl PersonaManager {
pub fn load_personas(config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
let shared = SelectionState::load_from_config(
config,
|cfg| &cfg.personas,
|cfg| &cfg.default_personas,
Config::set_default_persona,
Config::unset_default_persona,
"Persona",
)?;
Ok(PersonaManager { shared })
}
pub fn list_personas(&self) -> &Vec<Persona> {
self.shared.items()
}
pub fn find_persona_by_id(&self, id: &str) -> Option<&Persona> {
self.shared.find_by_id(id)
}
pub fn set_active_persona(&mut self, persona_id: &str) -> Result<(), String> {
self.shared.set_active(persona_id)
}
pub fn clear_active_persona(&mut self) {
self.shared.clear_active();
}
pub fn get_active_persona(&self) -> Option<&Persona> {
self.shared.get_active()
}
pub fn apply_substitutions(&self, text: &str, char_name: Option<&str>) -> String {
let char_replacement = char_name.unwrap_or("Assistant");
let user_replacement = self
.shared
.get_active()
.map(|persona| persona.display_name.as_str())
.unwrap_or("Anon");
text.replace("{{char}}", char_replacement)
.replace("{{user}}", user_replacement)
}
pub fn get_display_name(&self) -> String {
self.shared
.get_active()
.map(|persona| persona.display_name.clone())
.unwrap_or_else(|| "You".to_string())
}
pub fn get_modified_system_prompt(&self, base_prompt: &str, char_name: Option<&str>) -> String {
match self.shared.get_active() {
Some(persona) => {
if let Some(bio) = &persona.bio {
let substituted_bio = self.apply_substitutions(bio, char_name);
let trimmed_bio = substituted_bio.trim();
if trimmed_bio.is_empty() {
base_prompt.to_string()
} else {
format!("{}\n\n{}", trimmed_bio, base_prompt)
}
} else {
base_prompt.to_string()
}
}
None => base_prompt.to_string(),
}
}
pub fn get_default_for_provider_model(&self, provider: &str, model: &str) -> Option<&str> {
self.shared.get_default_for_provider_model(provider, model)
}
pub fn set_default_for_provider_model_persistent(
&mut self,
provider: &str,
model: &str,
persona_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.shared
.set_default_persistent(provider, model, persona_id)
}
pub fn unset_default_for_provider_model_persistent(
&mut self,
provider: &str,
model: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.shared.unset_default_persistent(provider, model)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> Config {
Config {
personas: vec![
Persona {
id: "alice-dev".to_string(),
display_name: "Alice".to_string(),
bio: Some("You are talking to {{user}}, a senior developer.".to_string()),
},
Persona {
id: "bob-student".to_string(),
display_name: "Bob".to_string(),
bio: Some(
"{{user}} is a computer science student learning about AI.".to_string(),
),
},
Persona {
id: "charlie-no-bio".to_string(),
display_name: "Charlie".to_string(),
bio: None,
},
],
..Default::default()
}
}
#[test]
fn test_persona_loading_from_configuration() {
let config = create_test_config();
let manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
assert_eq!(manager.list_personas().len(), 3);
assert!(manager.find_persona_by_id("alice-dev").is_some());
assert!(manager.find_persona_by_id("bob-student").is_some());
assert!(manager.find_persona_by_id("charlie-no-bio").is_some());
assert!(manager.find_persona_by_id("nonexistent").is_none());
}
#[test]
fn test_persona_activation_and_deactivation() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
assert!(manager.get_active_persona().is_none());
assert!(manager.set_active_persona("alice-dev").is_ok());
let active = manager.get_active_persona().expect("No active persona");
assert_eq!(active.id, "alice-dev");
assert_eq!(active.display_name, "Alice");
assert!(manager.set_active_persona("bob-student").is_ok());
let active = manager.get_active_persona().expect("No active persona");
assert_eq!(active.id, "bob-student");
assert_eq!(active.display_name, "Bob");
manager.clear_active_persona();
assert!(manager.get_active_persona().is_none());
}
#[test]
fn test_invalid_persona_id_error_handling() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
let result = manager.set_active_persona("nonexistent");
assert!(result.is_err());
let error_msg = result.unwrap_err();
assert!(error_msg.contains("Persona 'nonexistent' not found"));
assert!(error_msg.contains("alice-dev"));
assert!(error_msg.contains("bob-student"));
assert!(error_msg.contains("charlie-no-bio"));
}
#[test]
fn test_substitution_logic_with_no_persona() {
let config = create_test_config();
let manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
let text = "Hello {{user}}, meet {{char}}!";
let result = manager.apply_substitutions(text, Some("Assistant"));
assert_eq!(result, "Hello Anon, meet Assistant!");
let result_no_char = manager.apply_substitutions(text, None);
assert_eq!(result_no_char, "Hello Anon, meet Assistant!");
}
#[test]
fn test_substitution_logic_with_active_persona() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
manager
.set_active_persona("alice-dev")
.expect("Failed to activate persona");
let text = "Hello {{user}}, meet {{char}}!";
let result = manager.apply_substitutions(text, Some("ChatBot"));
assert_eq!(result, "Hello Alice, meet ChatBot!");
let result_no_char = manager.apply_substitutions(text, None);
assert_eq!(result_no_char, "Hello Alice, meet Assistant!");
}
#[test]
fn test_display_name_with_no_persona() {
let config = create_test_config();
let manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
assert_eq!(manager.get_display_name(), "You");
}
#[test]
fn test_display_name_with_active_persona() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
manager
.set_active_persona("alice-dev")
.expect("Failed to activate persona");
assert_eq!(manager.get_display_name(), "Alice");
manager
.set_active_persona("bob-student")
.expect("Failed to activate persona");
assert_eq!(manager.get_display_name(), "Bob");
}
#[test]
fn test_system_prompt_modification_no_persona() {
let config = create_test_config();
let manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
let base_prompt = "You are a helpful assistant.";
let result = manager.get_modified_system_prompt(base_prompt, None);
assert_eq!(result, base_prompt);
}
#[test]
fn test_system_prompt_modification_with_persona_bio() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
manager
.set_active_persona("alice-dev")
.expect("Failed to activate persona");
let base_prompt = "You are a helpful assistant.";
let result = manager.get_modified_system_prompt(base_prompt, None);
let expected =
"You are talking to Alice, a senior developer.\n\nYou are a helpful assistant.";
assert_eq!(result, expected);
}
#[test]
fn test_system_prompt_modification_with_persona_no_bio() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
manager
.set_active_persona("charlie-no-bio")
.expect("Failed to activate persona");
let base_prompt = "You are a helpful assistant.";
let result = manager.get_modified_system_prompt(base_prompt, None);
assert_eq!(result, base_prompt);
}
#[test]
fn test_system_prompt_modification_ignores_empty_or_whitespace_bio() {
let config = Config {
personas: vec![
Persona {
id: "dana-empty".to_string(),
display_name: "Dana".to_string(),
bio: Some(String::new()),
},
Persona {
id: "erin-whitespace".to_string(),
display_name: "Erin".to_string(),
bio: Some(" \n\t".to_string()),
},
],
..Default::default()
};
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
let base_prompt = "You are a helpful assistant.";
manager
.set_active_persona("dana-empty")
.expect("Failed to activate persona");
let result_empty = manager.get_modified_system_prompt(base_prompt, None);
assert_eq!(result_empty, base_prompt);
manager
.set_active_persona("erin-whitespace")
.expect("Failed to activate persona");
let result_whitespace = manager.get_modified_system_prompt(base_prompt, None);
assert_eq!(result_whitespace, base_prompt);
}
#[test]
fn test_persona_bio_substitution() {
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
manager
.set_active_persona("bob-student")
.expect("Failed to activate persona");
let base_prompt = "You are a helpful assistant.";
let result = manager.get_modified_system_prompt(base_prompt, None);
let expected =
"Bob is a computer science student learning about AI.\n\nYou are a helpful assistant.";
assert_eq!(result, expected);
}
#[test]
fn test_ui_display_name_integration() {
use crate::core::app::ui_state::UiState;
use crate::ui::theme::Theme;
let config = create_test_config();
let mut manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
let mut ui = UiState::new_basic(Theme::dark_default(), true, true, None);
assert_eq!(ui.user_display_name, "You");
manager
.set_active_persona("alice-dev")
.expect("Failed to activate persona");
let display_name = manager.get_display_name();
ui.update_user_display_name(display_name);
assert_eq!(ui.user_display_name, "Alice");
manager.clear_active_persona();
let display_name = manager.get_display_name();
ui.update_user_display_name(display_name);
assert_eq!(ui.user_display_name, "You");
}
#[test]
fn test_default_persona_loading_from_config() {
let mut config = create_test_config();
config.set_default_persona(
"openai".to_string(),
"gpt-4".to_string(),
"alice-dev".to_string(),
);
config.set_default_persona(
"anthropic".to_string(),
"claude-3-opus".to_string(),
"bob-student".to_string(),
);
let manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
assert_eq!(
manager.get_default_for_provider_model("openai", "gpt-4"),
Some("alice-dev")
);
assert_eq!(
manager.get_default_for_provider_model("anthropic", "claude-3-opus"),
Some("bob-student")
);
assert!(manager
.get_default_for_provider_model("openai", "gpt-3.5-turbo")
.is_none());
}
#[test]
fn test_default_persona_lookup_case_and_underscores() {
let mut config = create_test_config();
config.set_default_persona(
"OpenAI".to_string(),
"gpt_4o_mini".to_string(),
"alice-dev".to_string(),
);
config.set_default_persona(
"openai_lab".to_string(),
"research_model".to_string(),
"bob-student".to_string(),
);
config.set_default_persona(
"foo_bar".to_string(),
"baz".to_string(),
"charlie-no-bio".to_string(),
);
config.set_default_persona(
"foo".to_string(),
"bar_baz".to_string(),
"alice-dev".to_string(),
);
let manager = PersonaManager::load_personas(&config).expect("Failed to load personas");
assert_eq!(
manager.get_default_for_provider_model("OPENAI", "gpt_4o_mini"),
Some("alice-dev")
);
assert_eq!(
manager.get_default_for_provider_model("OpenAI_Lab", "research_model"),
Some("bob-student")
);
assert_eq!(
manager.get_default_for_provider_model("foo_bar", "baz"),
Some("charlie-no-bio")
);
assert_eq!(
manager.get_default_for_provider_model("foo", "bar_baz"),
Some("alice-dev")
);
}
}
#[test]
fn test_message_rendering_with_persona_display_name() {
use crate::core::message::Message;
use crate::ui::markdown::{render_message_with_config, MessageRenderConfig};
use crate::ui::theme::Theme;
let theme = Theme::dark_default();
let message = Message {
role: "user".to_string(),
content: "Hello world".to_string(),
};
let config_default = MessageRenderConfig::markdown(false, false);
let rendered_default = render_message_with_config(&message, &theme, config_default);
let first_line_default = rendered_default.lines.first().unwrap().to_string();
assert!(first_line_default.starts_with("You: "));
let config_persona = MessageRenderConfig::markdown(false, false)
.with_user_display_name(Some("Alice".to_string()));
let rendered_persona = render_message_with_config(&message, &theme, config_persona);
let first_line_persona = rendered_persona.lines.first().unwrap().to_string();
assert!(first_line_persona.starts_with("Alice: "));
}