From 3ead2bdd0d92083dc11fc49f9260183c2a0d79cd Mon Sep 17 00:00:00 2001 From: Victor Kyriazakos Date: Thu, 18 Jun 2026 19:19:48 +0300 Subject: [PATCH] feat(prompt): configurable per-platform system-prompt hint overrides Add platform_hints config so an admin can append to or replace Hermes' built-in platform hint for a single messaging platform (WhatsApp, Slack, Telegram, ...) without affecting other platforms. Enables enterprise managed profiles to steer platform-aware skills (e.g. invoke a custom table-formatting skill on WhatsApp where Markdown tables don't render) while leaving Telegram/Slack/CLI behavior unchanged. - hermes_cli/config.py: document platform_hints in DEFAULT_CONFIG - agent/agent_init.py: load platform_hints -> agent._platform_hint_overrides - agent/system_prompt.py: _resolve_platform_hint() applies append/replace (replace wins; bare string = append shorthand); defensive on bad config - tests: 16 cases covering append/replace/shorthand/isolation/malformed Override only affects the platform-hint segment of the system prompt; SOUL/context/memory tiers and general instructions are unchanged. --- agent/agent_init.py | 17 ++++ agent/system_prompt.py | 60 +++++++++++- hermes_cli/config.py | 16 ++++ scripts/release.py | 1 + tests/agent/test_platform_hint_overrides.py | 101 ++++++++++++++++++++ 5 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 tests/agent/test_platform_hint_overrides.py diff --git a/agent/agent_init.py b/agent/agent_init.py index 4210b515f65..555f930f559 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -1239,6 +1239,23 @@ def init_agent( # are noisy. agent._environment_probe = bool(_agent_section.get("environment_probe", True)) + # Per-platform prompt-hint overrides (config.yaml → platform_hints). + # Lets an enterprise admin append to or replace Hermes' built-in + # platform hint for a single messaging platform (e.g. WhatsApp) without + # affecting other platforms. Shape: + # platform_hints: + # whatsapp: + # append: "When tabular output would help, invoke the ... skill." + # slack: + # replace: "Custom Slack hint that fully replaces the default." + # Stored verbatim; resolution happens in agent/system_prompt.py against + # the active platform. Invalid shapes are ignored defensively so a bad + # config entry can never break prompt assembly. + _platform_hints_cfg = _agent_cfg.get("platform_hints", {}) + if not isinstance(_platform_hints_cfg, dict): + _platform_hints_cfg = {} + agent._platform_hint_overrides = _platform_hints_cfg + # App-level API retry count (wraps each model API call). Default 3, # overridable via agent.api_max_retries in config.yaml. See #11616. try: diff --git a/agent/system_prompt.py b/agent/system_prompt.py index 281f01399b4..d8eaea4e39e 100644 --- a/agent/system_prompt.py +++ b/agent/system_prompt.py @@ -61,6 +61,55 @@ def _ra(): return run_agent +def _resolve_platform_hint(agent: Any, platform_key: str, default_hint: str) -> str: + """Apply a per-platform prompt-hint override to the default hint. + + Reads ``agent._platform_hint_overrides`` (populated from + ``config.yaml`` ``platform_hints`` by ``agent_init``) and resolves the + effective hint for *platform_key*: + + * ``replace`` — substitute the default hint entirely. + * ``append`` — keep the default and append the extra text. + * a bare string value — treated as ``append`` (convenience shorthand). + + Precedence: ``replace`` wins over ``append`` if both are present. + Override text is added on top of (not instead of) the SOUL/context/ + memory tiers — it only affects the platform-hint segment, so other + platforms are unaffected and general system instructions still apply. + + Defensive: any malformed entry falls back to the unmodified default so + a bad config value can never break prompt assembly or leak across + platforms. + """ + if not platform_key: + return default_hint + overrides = getattr(agent, "_platform_hint_overrides", None) + if not isinstance(overrides, dict) or not overrides: + return default_hint + spec = overrides.get(platform_key) + if spec is None: + return default_hint + + # Shorthand: a bare string is treated as append text. + if isinstance(spec, str): + extra = spec.strip() + return f"{default_hint}\n\n{extra}".strip() if extra else default_hint + + if not isinstance(spec, dict): + return default_hint + + replace_text = spec.get("replace") + if isinstance(replace_text, str) and replace_text.strip(): + base = replace_text.strip() + else: + base = default_hint + + append_text = spec.get("append") + if isinstance(append_text, str) and append_text.strip(): + return f"{base}\n\n{append_text.strip()}".strip() + return base + + def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None) -> Dict[str, str]: """Assemble the system prompt as three ordered parts. @@ -331,18 +380,25 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None) ) platform_key = (agent.platform or "").lower().strip() + # Resolve the built-in/plugin default hint for this platform, then apply + # any per-platform override from config (platform_hints.). + _default_hint = "" if platform_key in PLATFORM_HINTS: - stable_parts.append(PLATFORM_HINTS[platform_key]) + _default_hint = PLATFORM_HINTS[platform_key] elif platform_key: # Check plugin registry for platform-specific LLM guidance try: from gateway.platform_registry import platform_registry _entry = platform_registry.get(platform_key) if _entry and _entry.platform_hint: - stable_parts.append(_entry.platform_hint) + _default_hint = _entry.platform_hint except Exception: pass + _effective_hint = _resolve_platform_hint(agent, platform_key, _default_hint) + if _effective_hint: + stable_parts.append(_effective_hint) + # ── Context tier (cwd-dependent, may change between sessions) ─ context_parts: List[str] = [] diff --git a/hermes_cli/config.py b/hermes_cli/config.py index c6975d39fe6..f698c11d5ac 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2170,6 +2170,22 @@ DEFAULT_CONFIG = { # User-defined quick commands that bypass the agent loop (type: exec only) "quick_commands": {}, + # Per-platform system-prompt hint overrides. Lets an admin append to or + # replace Hermes' built-in platform hint for a single messaging platform + # (WhatsApp, Slack, Telegram, ...) without affecting other platforms. + # Useful for enterprise/managed profiles that ship platform-aware skills. + # Each key is a platform name; the value is either: + # { "append": "extra text" } — keep the default hint, append text + # { "replace": "full text" } — substitute the default hint entirely + # "extra text" — shorthand for { "append": ... } + # `replace` wins over `append` if both are given. Example: + # platform_hints: + # whatsapp: + # append: > + # When tabular output would be useful, invoke the + # table_formatting skill instead of emitting a Markdown table. + "platform_hints": {}, + # Shell-script hooks — declarative bridge that invokes shell scripts # on plugin-hook events (pre_tool_call, post_tool_call, pre_llm_call, # subagent_stop, etc.). Each entry maps an event name to a list of diff --git a/scripts/release.py b/scripts/release.py index 79ecf36382a..6f56a14154d 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { + "victor@rocketfueldev.com": "victor-kyriazakos", "286497132+srojk34@users.noreply.github.com": "srojk34", "59806492+sitkarev@users.noreply.github.com": "sitkarev", "zheng@omegasys.eu": "omegazheng", diff --git a/tests/agent/test_platform_hint_overrides.py b/tests/agent/test_platform_hint_overrides.py new file mode 100644 index 00000000000..fe34669ee03 --- /dev/null +++ b/tests/agent/test_platform_hint_overrides.py @@ -0,0 +1,101 @@ +"""Tests for per-platform prompt-hint overrides (config.yaml → platform_hints). + +Covers agent/system_prompt.py::_resolve_platform_hint — the resolver that +applies append/replace overrides to a platform's default hint. Feature added +for enterprise managed profiles (per-platform behavior without affecting other +platforms). See HA Core ticket: configurable per-platform prompt hints. +""" + +import types + +from agent.system_prompt import _resolve_platform_hint + + +def _agent(overrides): + """Minimal stand-in carrying just the override attribute the resolver reads.""" + a = types.SimpleNamespace() + a._platform_hint_overrides = overrides + return a + + +DEFAULT = "You are on WhatsApp. Do not use markdown." +EXTRA = "When tabular output would help, invoke the table_formatting skill." + + +class TestResolvePlatformHint: + def test_no_overrides_returns_default(self): + assert _resolve_platform_hint(_agent({}), "whatsapp", DEFAULT) == DEFAULT + + def test_missing_attr_returns_default(self): + a = types.SimpleNamespace() # no _platform_hint_overrides at all + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_platform_not_in_overrides_returns_default(self): + a = _agent({"slack": {"append": "x"}}) + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_append_dict(self): + a = _agent({"whatsapp": {"append": EXTRA}}) + out = _resolve_platform_hint(a, "whatsapp", DEFAULT) + assert out == f"{DEFAULT}\n\n{EXTRA}" + assert DEFAULT in out and EXTRA in out + + def test_replace_dict(self): + a = _agent({"whatsapp": {"replace": EXTRA}}) + out = _resolve_platform_hint(a, "whatsapp", DEFAULT) + assert out == EXTRA + assert DEFAULT not in out + + def test_replace_wins_over_append_but_both_applied(self): + a = _agent({"whatsapp": {"replace": "BASE", "append": "TAIL"}}) + out = _resolve_platform_hint(a, "whatsapp", DEFAULT) + # replace substitutes the base, append still tacks on + assert out == "BASE\n\nTAIL" + assert DEFAULT not in out + + def test_bare_string_is_append_shorthand(self): + a = _agent({"whatsapp": EXTRA}) + out = _resolve_platform_hint(a, "whatsapp", DEFAULT) + assert out == f"{DEFAULT}\n\n{EXTRA}" + + def test_other_platform_unaffected(self): + """An override for whatsapp must not change telegram's hint.""" + a = _agent({"whatsapp": {"append": EXTRA}}) + tg_default = "You are on Telegram. Markdown works." + assert _resolve_platform_hint(a, "telegram", tg_default) == tg_default + + def test_empty_platform_key_returns_default(self): + a = _agent({"whatsapp": {"append": EXTRA}}) + assert _resolve_platform_hint(a, "", DEFAULT) == DEFAULT + + # --- defensive / malformed input: never break prompt assembly --- + + def test_malformed_spec_list_returns_default(self): + a = _agent({"whatsapp": ["not", "valid"]}) + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_overrides_not_a_dict_returns_default(self): + a = _agent(["nope"]) + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_empty_append_string_returns_default(self): + a = _agent({"whatsapp": {"append": " "}}) + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_empty_replace_falls_back_to_default_base(self): + a = _agent({"whatsapp": {"replace": " "}}) + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_non_string_append_ignored(self): + a = _agent({"whatsapp": {"append": 123}}) + assert _resolve_platform_hint(a, "whatsapp", DEFAULT) == DEFAULT + + def test_replace_with_empty_default_hint(self): + """replace works even when the platform had no built-in default.""" + a = _agent({"customplat": {"replace": "Custom hint."}}) + assert _resolve_platform_hint(a, "customplat", "") == "Custom hint." + + def test_append_with_empty_default_hint(self): + """append on a platform with no default just yields the extra text.""" + a = _agent({"customplat": {"append": "Only this."}}) + assert _resolve_platform_hint(a, "customplat", "") == "Only this."