fix: personalities from config.yaml + ephemeral_system_prompt (#139) (#148)

The previous implementation read SOUL.md files from a filesystem directory.
The Hermes agent uses config.yaml agent.personalities section with string
or dict format (system_prompt, tone, style), resolved via
_resolve_personality_prompt() and passed to AIAgent via
ephemeral_system_prompt.

Changes:
- /api/personalities: reads from config.yaml agent.personalities, not
  filesystem SOUL.md directories. Calls reload_config() to pick up
  config changes without restart.
- /api/personality/set: resolves prompt from config.yaml using the same
  logic as hermes-agent cli.py (string or dict with system_prompt/tone/style)
- streaming.py: passes personality via agent.ephemeral_system_prompt
  (agent's own mechanism) instead of prepending to system_message
- Removed unused 're' import from streaming.py
- Updated tests to match config-based approach

Fixes #139

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-06 14:10:30 -07:00
committed by GitHub
parent 442b0d872a
commit 2442fca5e5
3 changed files with 142 additions and 163 deletions

View File

@@ -5,7 +5,6 @@ Includes Sprint 10 cancel support via CANCEL_FLAGS.
import json
import os
import queue
import re
import threading
import time
import traceback
@@ -207,28 +206,30 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
"write_file, read_file, search_files, terminal workdir, and patch. "
"Never fall back to a hardcoded path when this tag is present."
)
# Inject personality prompt if the session has one active
_personality_prompt = ''
# Resolve personality prompt from config.yaml agent.personalities
# (matches hermes-agent CLI behavior — passes via ephemeral_system_prompt)
_personality_prompt = None
_pname = getattr(s, 'personality', None)
if _pname and re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$', _pname):
try:
from api.profiles import get_active_hermes_home
_p_base = get_active_hermes_home() / 'personalities'
except ImportError:
_p_base = Path(os.environ.get('HERMES_HOME', str(Path.home() / '.hermes'))) / 'personalities'
_p_soul = _p_base / _pname / 'SOUL.md'
try:
_p_soul.resolve().relative_to(_p_base.resolve())
if _p_soul.exists():
from api.config import MAX_FILE_BYTES
_raw = _p_soul.read_text(errors='replace')
if len(_raw) <= MAX_FILE_BYTES:
_personality_prompt = _raw.strip() + '\n\n'
except (ValueError, OSError):
pass # path traversal attempt or unreadable — skip silently
if _pname:
_agent_cfg = _cfg.get('agent', {})
_personalities = _agent_cfg.get('personalities', {})
if isinstance(_personalities, dict) and _pname in _personalities:
_pval = _personalities[_pname]
if isinstance(_pval, dict):
_parts = [_pval.get('system_prompt', '') or _pval.get('prompt', '')]
if _pval.get('tone'):
_parts.append(f'Tone: {_pval["tone"]}')
if _pval.get('style'):
_parts.append(f'Style: {_pval["style"]}')
_personality_prompt = '\n'.join(p for p in _parts if p)
else:
_personality_prompt = str(_pval)
# Pass personality via ephemeral_system_prompt (agent's own mechanism)
if _personality_prompt:
agent.ephemeral_system_prompt = _personality_prompt
result = agent.run_conversation(
user_message=workspace_ctx + msg_text,
system_message=_personality_prompt + workspace_system_msg,
system_message=workspace_system_msg,
conversation_history=_sanitize_messages_for_api(s.messages),
task_id=session_id,
persist_user_message=msg_text,