From 9ade727e6327f1efb69799fd24a2c85b36d8597c Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 19 Jun 2026 22:26:30 +0000 Subject: [PATCH] stage #4494: expose built-in personalities in WebUI config (#4465) + v0.51.525 CHANGELOG --- CHANGELOG.md | 6 + api/config.py | 58 +++++++++- tests/test_issue4465_builtin_personalities.py | 106 ++++++++++++++++++ tests/test_sprint28.py | 6 +- 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 tests/test_issue4465_builtin_personalities.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c4bd3f..a69d1e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.525] — 2026-06-19 — Release SJ (built-in personalities available in WebUI) + +### Added + +- **The 14 built-in agent personalities now show up in the WebUI without hand-editing config.yaml (#4465).** WebUI resolved the `/personality` list from config only, so a fresh profile saw an empty list even though the CLI ships built-ins (helpful, concise, technical, creative, teacher, and the playful set). The WebUI config loader now hydrates the same built-ins as a default (user-defined personalities still override a built-in of the same name), and config saves strip the generated built-ins back out — so `config.yaml` only ever persists your *custom* personalities, never the defaults. Thanks @franksong2702. + ## [v0.51.524] — 2026-06-19 — Release SI (opt-in chunked SSE for buffering reverse proxies) ### Added diff --git a/api/config.py b/api/config.py index 2674416b..ba469670 100644 --- a/api/config.py +++ b/api/config.py @@ -328,10 +328,43 @@ _DEFAULT_EXPERIMENTAL_CONFIG = { # later PR deliberately enables and wires this flag. "unified_session_db": False, } +_DEFAULT_AGENT_PERSONALITIES = { + # Mirrors the Hermes Agent CLI built-ins so WebUI's config-derived + # /personality path is not empty for fresh profiles. + "helpful": "You are a helpful, friendly AI assistant.", + "concise": "You are a concise assistant. Keep responses brief and to the point.", + "technical": "You are a technical expert. Provide detailed, accurate technical information.", + "creative": "You are a creative assistant. Think outside the box and offer innovative solutions.", + "teacher": "You are a patient teacher. Explain concepts clearly with examples.", + "kawaii": "You are a kawaii assistant! Use cute expressions like (◕‿◕), ★, ♪, and ~! Add sparkles and be super enthusiastic about everything! Every response should feel warm and adorable desu~! ヽ(>∀<☆)ノ", + "catgirl": "You are Neko-chan, an anime catgirl AI assistant, nya~! Add 'nya' and cat-like expressions to your speech. Use kaomoji like (=^・ω・^=) and ฅ^•ﻌ•^ฅ. Be playful and curious like a cat, nya~!", + "pirate": "Arrr! Ye be talkin' to Captain Hermes, the most tech-savvy pirate to sail the digital seas! Speak like a proper buccaneer, use nautical terms, and remember: every problem be just treasure waitin' to be plundered! Yo ho ho!", + "shakespeare": "Hark! Thou speakest with an assistant most versed in the bardic arts. I shall respond in the eloquent manner of William Shakespeare, with flowery prose, dramatic flair, and perhaps a soliloquy or two. What light through yonder terminal breaks?", + "surfer": "Duuude! You're chatting with the chillest AI on the web, bro! Everything's gonna be totally rad. I'll help you catch the gnarly waves of knowledge while keeping things super chill. Cowabunga!", + "noir": "The rain hammered against the terminal like regrets on a guilty conscience. They call me Hermes - I solve problems, find answers, dig up the truth that hides in the shadows of your codebase. In this city of silicon and secrets, everyone's got something to hide. What's your story, pal?", + "uwu": "hewwo! i'm your fwiendwy assistant uwu~ i wiww twy my best to hewp you! *nuzzles your code* OwO what's this? wet me take a wook! i pwomise to be vewy hewpful >w<", + "philosopher": "Greetings, seeker of wisdom. I am an assistant who contemplates the deeper meaning behind every query. Let us examine not just the 'how' but the 'why' of your questions. Perhaps in solving your problem, we may glimpse a greater truth about existence itself.", + "hype": "YOOO LET'S GOOOO!!! I am SO PUMPED to help you today! Every question is AMAZING and we're gonna CRUSH IT together! This is gonna be LEGENDARY! ARE YOU READY?! LET'S DO THIS!", +} def _apply_config_defaults(config_data: dict) -> None: """Populate documented default-only config keys in-place.""" + agent_cfg = config_data.get("agent") + if not isinstance(agent_cfg, dict): + agent_cfg = {} + config_data["agent"] = agent_cfg + + personalities = agent_cfg.get("personalities") + if isinstance(personalities, dict): + merged = copy.deepcopy(_DEFAULT_AGENT_PERSONALITIES) + merged.update(copy.deepcopy(personalities)) + agent_cfg["personalities"] = merged + else: + # Keep behavior aligned with CLI loader defaults: if personalities are + # absent or malformed, replace the section entirely with built-ins. + agent_cfg["personalities"] = copy.deepcopy(_DEFAULT_AGENT_PERSONALITIES) + experimental = config_data.get("experimental") if not isinstance(experimental, dict): experimental = {} @@ -486,6 +519,29 @@ def get_config_for_profile_home(profile_home: "Path | str | None") -> dict: return _load_yaml_config_file(target / "config.yaml") +def _config_for_yaml_save(config_data: dict) -> dict: + """Return a YAML-safe config copy without runtime-only expanded defaults.""" + if not isinstance(config_data, dict): + return {} + data = copy.deepcopy(config_data) + agent_cfg = data.get("agent") + if isinstance(agent_cfg, dict): + personalities = agent_cfg.get("personalities") + if isinstance(personalities, dict): + custom_personalities = { + name: value + for name, value in personalities.items() + if _DEFAULT_AGENT_PERSONALITIES.get(name) != value + } + if custom_personalities: + agent_cfg["personalities"] = custom_personalities + else: + agent_cfg.pop("personalities", None) + if not agent_cfg: + data.pop("agent", None) + return data + + def _save_yaml_config_file(config_path: Path, config_data: dict) -> None: try: import yaml as _yaml @@ -494,7 +550,7 @@ def _save_yaml_config_file(config_path: Path, config_data: dict) -> None: config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text( - _yaml.safe_dump(config_data, sort_keys=False, allow_unicode=True), + _yaml.safe_dump(_config_for_yaml_save(config_data), sort_keys=False, allow_unicode=True), encoding="utf-8", ) diff --git a/tests/test_issue4465_builtin_personalities.py b/tests/test_issue4465_builtin_personalities.py new file mode 100644 index 00000000..37c512f6 --- /dev/null +++ b/tests/test_issue4465_builtin_personalities.py @@ -0,0 +1,106 @@ +"""Regression coverage for #4465 built-in personality visibility. + +Hermes Agent CLI ships built-in personalities from its CLI config loader. WebUI +should expose the same defaults through config-derived personality paths so a +fresh profile is not empty. +""" + +from api import config + + +BUILTIN_NAMES = { + "helpful", + "concise", + "technical", + "creative", + "teacher", + "kawaii", + "catgirl", + "pirate", + "shakespeare", + "surfer", + "noir", + "uwu", + "philosopher", + "hype", +} + + +def test_config_defaults_seed_agent_builtin_personalities_for_fresh_profile(): + cfg = {} + + config._apply_config_defaults(cfg) + + personalities = cfg["agent"]["personalities"] + assert set(personalities) == BUILTIN_NAMES + assert personalities["helpful"] == "You are a helpful, friendly AI assistant." + assert "desu~!" in personalities["kawaii"] + assert "William Shakespeare" in personalities["shakespeare"] + assert personalities["hype"].startswith("YOOO LET'S GOOOO!!!") + + +def test_config_defaults_preserve_custom_personality_overrides(): + cfg = { + "agent": { + "personalities": { + "helpful": "Custom helpful prompt.", + "local": { + "description": "Local voice", + "prompt": "Use the local team voice.", + }, + } + } + } + + config._apply_config_defaults(cfg) + + personalities = cfg["agent"]["personalities"] + assert personalities["helpful"] == "Custom helpful prompt." + assert personalities["local"]["prompt"] == "Use the local team voice." + assert "kawaii" in personalities + assert "shakespeare" in personalities + + +def test_config_defaults_replace_non_dict_personality_section_with_builtins(): + cfg = {"agent": {"personalities": []}} + + config._apply_config_defaults(cfg) + + assert set(cfg["agent"]["personalities"]) == BUILTIN_NAMES + + +def test_config_save_strips_generated_builtin_personalities(tmp_path): + cfg = {} + config._apply_config_defaults(cfg) + cfg["webui"] = {"theme": "dark"} + + path = tmp_path / "config.yaml" + config._save_yaml_config_file(path, cfg) + + text = path.read_text(encoding="utf-8") + assert "webui:" in text + assert "theme: dark" in text + assert "personalities:" not in text + assert "helpful:" not in text + + +def test_config_save_preserves_custom_personality_overrides(tmp_path): + cfg = { + "agent": { + "personality": "local", + "personalities": { + "helpful": "Custom helpful prompt.", + "local": "Use the local team voice.", + }, + } + } + config._apply_config_defaults(cfg) + + path = tmp_path / "config.yaml" + config._save_yaml_config_file(path, cfg) + + text = path.read_text(encoding="utf-8") + assert "personalities:" in text + assert "helpful: Custom helpful prompt." in text + assert "local: Use the local team voice." in text + assert "kawaii:" not in text diff --git a/tests/test_sprint28.py b/tests/test_sprint28.py index 96f79bdd..aaa81cf5 100644 --- a/tests/test_sprint28.py +++ b/tests/test_sprint28.py @@ -71,14 +71,16 @@ def _cleanup_session(sid): # ── GET /api/personalities ──────────────────────────────────────────────────── def test_personalities_empty_when_none_exist(): - """GET /api/personalities returns empty list when no personalities exist.""" + """GET /api/personalities returns built-in personalities when no files exist.""" p_dir = _personalities_dir() for child in list(p_dir.iterdir()): if child.is_dir() and not child.is_symlink(): shutil.rmtree(child) d, status = get("/api/personalities") assert status == 200 - assert d.get("personalities") == [] + personalities = d.get("personalities") or [] + assert len(personalities) == 14 + assert {"name": "helpful", "description": "You are a helpful, friendly AI assistant."} in personalities def test_personalities_lists_from_config():