stage #4494: expose built-in personalities in WebUI config (#4465) + v0.51.525 CHANGELOG

This commit is contained in:
nesquena-hermes committed 2026-06-19 22:26:30 +00:00
1 parent 164fd7ff20
commit 9ade727e63
4 files changed
+173 -3

No files matched your search

+6
View File
@@ -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
+57 -1
View File
@@ -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",
)
@@ -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
+4 -2
View File
@@ -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():