use crate::entities::profile::ToolId;
use crate::entities::profile::{CharacterNames, Profile};
use crate::entities::sampling::SamplingConfig;
use crate::features::tools::default_tool_ids;
pub const MAX_NAME_LEN: usize = 80;
pub fn sanitize_name(input: &str) -> Option<String> {
let collapsed: String = input
.chars()
.map(|c| if c.is_whitespace() { ' ' } else { c })
.collect();
let mut name = String::with_capacity(collapsed.len());
let mut prev_space = false;
for ch in collapsed.trim().chars() {
let is_space = ch == ' ';
if is_space && prev_space {
continue;
}
name.push(ch);
prev_space = is_space;
}
if name.is_empty() {
return None;
}
Some(name.chars().take(MAX_NAME_LEN).collect())
}
pub fn create(name: &str, system_message: impl Into<String>) -> Option<Profile> {
let name = sanitize_name(name)?;
let mut profile = Profile::new(name, system_message);
reconcile_tools(&mut profile);
Some(profile)
}
pub fn reconcile_tools(profile: &mut Profile) -> bool {
if profile.known_tools.is_empty() {
profile.known_tools = profile.enabled_tools.clone();
}
let mut changed = false;
for id in default_tool_ids() {
if profile.known_tools.iter().any(|t| t == &id) {
continue; }
profile.known_tools.push(id.clone());
changed = true;
if !profile.enabled_tools.iter().any(|t| t == &id) {
profile.enabled_tools.push(id);
}
}
changed
}
const SEED_CHARACTER_NAMES: [(&str, &str, &str); 2] = [
("Вы", "Ассистент", "Система"), ("You", "Assistant", "System"),
];
pub fn clear_seed_character_names(profile: &mut Profile) -> bool {
let names = &mut profile.character_names;
let mut changed = false;
for (user, assistant, system) in SEED_CHARACTER_NAMES {
for (field, seed) in [
(&mut names.user, user),
(&mut names.assistant, assistant),
(&mut names.system, system),
] {
if field == seed {
field.clear();
changed = true;
}
}
}
changed
}
pub fn reseed_language_defaults(
profile: &mut Profile,
old_lang: crate::shared::i18n::Lang,
) -> bool {
let old = crate::shared::i18n::locale(old_lang);
let new = crate::shared::i18n::locale(profile.language);
let mut changed = false;
if profile.name == old.t("defaults.profile_name") {
profile.name = new.t("defaults.profile_name").to_string();
changed = true;
}
if profile.default_system_message == old.t("defaults.system_message") {
profile.default_system_message = new.t("defaults.system_message").to_string();
changed = true;
}
changed
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ProfileEdit {
pub name: Option<String>,
pub system_message: Option<String>,
pub impersonation_profile_id: Option<Option<uuid::Uuid>>,
pub greeting: Option<Option<String>>,
pub character_names: Option<CharacterNames>,
pub default_sampling: Option<Option<SamplingConfig>>,
pub enabled_tools: Option<Vec<ToolId>>,
pub language: Option<crate::shared::i18n::Lang>,
}
pub fn apply_edit(profile: &mut Profile, edit: ProfileEdit) -> bool {
let new_name = match &edit.name {
Some(raw) => match sanitize_name(raw) {
Some(n) => Some(n),
None => return false,
},
None => None,
};
if let Some(name) = new_name {
profile.name = name;
}
if let Some(system) = edit.system_message {
profile.default_system_message = system;
}
if let Some(imp) = edit.impersonation_profile_id {
profile.impersonation_profile_id = imp;
}
if let Some(greeting) = edit.greeting {
profile.greeting = greeting.filter(|g| !g.is_empty());
}
if let Some(names) = edit.character_names {
profile.character_names = names;
}
if let Some(sampling) = edit.default_sampling {
profile.default_sampling = sampling;
}
if let Some(tools) = edit.enabled_tools {
profile.enabled_tools = tools;
}
if let Some(lang) = edit.language {
profile.language = lang;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_collapses_and_trims() {
assert_eq!(
sanitize_name(" Джойс Кэрол ").as_deref(),
Some("Джойс Кэрол")
);
assert_eq!(sanitize_name(" \t\n "), None);
}
#[test]
fn sanitize_truncates() {
let long = "я".repeat(MAX_NAME_LEN + 20);
assert_eq!(sanitize_name(&long).unwrap().chars().count(), MAX_NAME_LEN);
}
#[test]
fn create_rejects_empty_name() {
assert!(create(" ", "sys").is_none());
let p = create(" Карлос ", "Ты — Карлос.").unwrap();
assert_eq!(p.name, "Карлос");
assert_eq!(p.default_system_message, "Ты — Карлос.");
}
#[test]
fn clear_seed_character_names_drops_only_seeds() {
let mut p = Profile::new("X", "sys");
p.character_names = CharacterNames {
user: "Вы".into(),
assistant: "Assistant".into(),
system: "Система".into(),
};
assert!(clear_seed_character_names(&mut p));
assert_eq!(p.character_names, CharacterNames::default());
assert!(!clear_seed_character_names(&mut p));
let mut custom = Profile::new("X", "sys");
custom.character_names = CharacterNames {
user: "Гайя".into(),
assistant: "Анна".into(),
system: String::new(),
};
let before = custom.character_names.clone();
assert!(!clear_seed_character_names(&mut custom));
assert_eq!(custom.character_names, before);
}
#[test]
fn apply_edit_updates_selected_fields() {
let mut p = Profile::new("X", "sys");
let ok = apply_edit(
&mut p,
ProfileEdit {
name: Some(" Новый ".into()),
system_message: Some("новое sys".into()),
greeting: Some(Some("Привет!".into())),
..Default::default()
},
);
assert!(ok);
assert_eq!(p.name, "Новый");
assert_eq!(p.default_system_message, "новое sys");
assert_eq!(p.greeting.as_deref(), Some("Привет!"));
}
#[test]
fn apply_edit_empty_greeting_clears_it() {
let mut p = Profile::new("X", "sys");
p.greeting = Some("было".into());
apply_edit(
&mut p,
ProfileEdit {
greeting: Some(Some(String::new())),
..Default::default()
},
);
assert_eq!(p.greeting, None);
}
#[test]
fn reseed_replaces_untouched_defaults() {
use crate::shared::i18n::{Lang, locale};
let ru = locale(Lang::Ru);
let mut p = Profile::new(
ru.t("defaults.profile_name"),
ru.t("defaults.system_message"),
);
p.language = Lang::En;
assert!(reseed_language_defaults(&mut p, Lang::Ru));
let en = locale(Lang::En);
assert_eq!(p.name, en.t("defaults.profile_name"));
assert_eq!(p.default_system_message, en.t("defaults.system_message"));
}
#[test]
fn reseed_keeps_user_edited_texts() {
use crate::shared::i18n::Lang;
let mut p = Profile::new("Гея", "Ты — Гея.");
p.language = Lang::En;
assert!(!reseed_language_defaults(&mut p, Lang::Ru));
assert_eq!(p.name, "Гея");
assert_eq!(p.default_system_message, "Ты — Гея.");
}
#[test]
fn reconcile_adds_new_tools_to_existing_profile() {
let mut p = Profile::new("X", "sys");
p.enabled_tools = vec!["note_save".into(), "web_search".into()];
let changed = reconcile_tools(&mut p);
assert!(changed);
assert!(p.enabled_tools.iter().any(|t| t == "calculate"));
assert!(p.enabled_tools.iter().any(|t| t == "current_time"));
assert!(p.enabled_tools.iter().any(|t| t == "note_save"));
for id in default_tool_ids() {
assert!(p.known_tools.iter().any(|t| t == &id), "not recorded: {id}");
}
}
#[test]
fn reconcile_does_not_reenable_user_disabled_tool() {
let mut p = Profile::new("X", "sys");
p.known_tools = default_tool_ids();
p.enabled_tools = default_tool_ids()
.into_iter()
.filter(|t| t != "calculate")
.collect();
let changed = reconcile_tools(&mut p);
assert!(
!changed,
"nothing new — a known disabled tool is left alone"
);
assert!(
!p.enabled_tools.iter().any(|t| t == "calculate"),
"a disabled tool shouldn't be re-enabled"
);
}
#[test]
fn reconcile_is_idempotent() {
let mut p = Profile::new("X", "sys");
assert!(reconcile_tools(&mut p)); let after_first = p.clone();
assert!(!reconcile_tools(&mut p)); assert_eq!(p, after_first);
}
#[test]
fn create_gives_default_tools() {
let p = create("Имя", "sys").unwrap();
assert!(p.enabled_tools.iter().any(|t| t == "calculate"));
assert_eq!(p.enabled_tools, default_tool_ids());
}
#[test]
fn apply_edit_rejects_empty_name_without_mutating() {
let mut p = Profile::new("Имя", "sys");
let ok = apply_edit(
&mut p,
ProfileEdit {
name: Some(" ".into()),
system_message: Some("не должно примениться".into()),
..Default::default()
},
);
assert!(!ok);
assert_eq!(p.name, "Имя");
assert_eq!(p.default_system_message, "sys");
}
}