mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-20 10:11:58 +00:00
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.
This commit is contained in:
parent
2944b3c394
commit
3ead2bdd0d
5 changed files with 193 additions and 2 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
101
tests/agent/test_platform_hint_overrides.py
Normal file
101
tests/agent/test_platform_hint_overrides.py
Normal 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."
|
||||
Loading…
Add table
Add a link
Reference in a new issue