Compare commits

...

2 Commits

Author SHA1 Message Date
teknium1
51dc40208a
docs(prompt): document platform_hints config override
Adds a 'Customizing platform hints' section to the Prompt Assembly
developer guide covering the append/replace/shorthand shapes, the
defensive fallback, and the cache-stable lifecycle (stable tier,
resolved at build time).
2026-06-18 13:49:03 -07:00
Victor Kyriazakos
2e10051178
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.
2026-06-18 13:48:19 -07:00
6 changed files with 230 additions and 2 deletions

View File

@ -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:

View File

@ -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.<platform>).
_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] = []

View File

@ -2155,6 +2155,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

View File

@ -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",

View File

@ -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."

View File

@ -116,6 +116,43 @@ You are a CLI AI Agent. Try not to use markdown but simple text
renderable inside a terminal.
```
## Customizing platform hints
The platform hint (Layer 10 above) is the per-surface guidance Hermes
injects for Telegram, WhatsApp, Slack, CLI, and other platforms — for
example "you are on a terminal, avoid Markdown." The built-in defaults
live in `PLATFORM_HINTS` (`agent/system_prompt.py`); plugin-provided
platforms supply theirs through the platform registry.
An administrator can append to or replace a single platform's hint from
`config.yaml` via the top-level `platform_hints` key, without touching
any other platform:
```yaml
platform_hints:
whatsapp:
append: >
When tabular output would be useful, invoke the table_formatting
skill instead of emitting a Markdown table.
slack:
replace: "You are on Slack. Keep responses tight and avoid wide tables."
telegram: "Prefer short messages; split long answers." # shorthand = append
```
- `append` — keep the built-in hint and add the extra text after it.
- `replace` — substitute the built-in hint entirely.
- A bare string — shorthand for `append`.
- `replace` wins over `append` when both are present.
- A malformed entry is ignored defensively and falls back to the
unmodified default, so a bad config value can never break prompt
assembly or leak across platforms.
The override is resolved when the system prompt is built (session start,
and again on compaction since that rebuilds the prompt). It produces a
byte-stable hint for a fixed config, so it lives in the **stable** tier
alongside the built-in hint and does not break prompt caching — it is
not a live mid-session mutation of a frozen prompt.
## How SOUL.md appears in the prompt
`SOUL.md` lives at `~/.hermes/SOUL.md` and serves as the agent's identity — the very first section of the system prompt. The loading logic in `prompt_builder.py` works as follows: