feat: load operator instructions from agents/*.md into the reply prompt
Some checks failed
Release / semantic-release (push) Successful in 30s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / build-linux (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
tests / Unit tests (Linux, Python 3.11) (push) Has been cancelled
Some checks failed
Release / semantic-release (push) Successful in 30s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / build-linux (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
tests / Unit tests (Linux, Python 3.11) (push) Has been cancelled
Drop Markdown files into an agents/ folder and their contents are appended to the main reply LLM's system prompt, so an operator can extend the assistant's rules/tone without code changes. Files are concatenated in filename order (use 00-, 10- prefixes to control ordering) and re-read once per turn, so edits apply on the next reply with no rebuild/restart. Fail-open: a missing, empty, or unreadable folder yields no instructions and never breaks a reply. - load_agent_instructions() in system_prompt.py (AGENTS_DIR env, default /app/agents); reads *.md only, skips blanks, ignores non-dir paths - engine.py appends it alongside the existing settings-UI llm_instructions, under the same "Additional instructions from the operator:" framing - docker-compose.yml bind-mounts ./agents:/app/agents:ro and sets AGENTS_DIR - agents/example.md.sample starter template (.sample is not loaded) - tests cover ordering, md-only filtering, blank-skip, env/arg resolution, and fail-open paths - README, .env.example, docs/llm_contexts.md updated Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,11 @@ import os
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from ..utils.redact import redact
|
||||
from ..system_prompt import build_system_prompt, reply_language_directive
|
||||
from ..system_prompt import (
|
||||
build_system_prompt,
|
||||
load_agent_instructions,
|
||||
reply_language_directive,
|
||||
)
|
||||
from ..tools.registry import run_tool_with_retries, generate_tools_description, generate_tools_json_schema, BUILTIN_TOOLS
|
||||
from ..tools.builtin.stop import STOP_SIGNAL
|
||||
from ..debug import debug_log
|
||||
@@ -1702,6 +1706,10 @@ def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
|
||||
# the directive used the config value made the two contradict each other.
|
||||
_output_language = _resolve_output_language()
|
||||
_persona_prompt = build_system_prompt(_assistant_name, _output_language)
|
||||
# File-based operator instructions: every *.md in AGENTS_DIR (default
|
||||
# /app/agents, bind-mounted from ./agents). Read once per turn so edits in
|
||||
# the folder apply on the next reply without a restart; fail-open to "".
|
||||
_agent_instructions = load_agent_instructions()
|
||||
|
||||
def _build_initial_system_message() -> str:
|
||||
guidance = [_persona_prompt.strip()]
|
||||
@@ -1810,6 +1818,12 @@ def run_reply_engine(db: "Database", cfg, tts: Optional[Any],
|
||||
if _user_instructions:
|
||||
guidance.append("Additional instructions from the operator:\n" + _user_instructions)
|
||||
|
||||
# File-based operator instructions: the concatenated agents/*.md content
|
||||
# resolved once above. Same framing/placement as the settings-UI field
|
||||
# so both are treated as authoritative operator guidance.
|
||||
if _agent_instructions:
|
||||
guidance.append("Additional instructions from the operator:\n" + _agent_instructions)
|
||||
|
||||
# Recency reinforcement: repeat the language lock at the very END too.
|
||||
# In a ~5k-token prompt the front-placed rule gets "lost in the middle";
|
||||
# bigger models (qwen2.5:7b) otherwise leak Chinese/Cyrillic mid-reply.
|
||||
|
||||
@@ -6,8 +6,51 @@ who renames the wake word (e.g. "Friday") gets a butler with the matching
|
||||
name rather than a persona hardcoded to "Jarvis".
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Default location of the operator's file-based instruction folder. In the
|
||||
# Docker deployment ./agents is bind-mounted here (see docker-compose.yml), so a
|
||||
# user can drop *.md files in without rebuilding. Overridable via AGENTS_DIR.
|
||||
_DEFAULT_AGENTS_DIR = "/app/agents"
|
||||
|
||||
|
||||
def load_agent_instructions(agents_dir: Optional[str] = None) -> str:
|
||||
"""Concatenate every ``*.md`` in the agents dir into one instruction block.
|
||||
|
||||
Files are read in filename order (so ``00-tone.md`` precedes ``10-rules.md``)
|
||||
and joined with blank lines. This lets the operator extend the main reply
|
||||
LLM's system prompt by dropping Markdown files into a folder, no code change
|
||||
or restart required — the caller reads this once per turn.
|
||||
|
||||
Resolution order for the directory: explicit ``agents_dir`` arg, then the
|
||||
``AGENTS_DIR`` env var, then ``/app/agents``.
|
||||
|
||||
Fail-open by design: a missing or empty directory, an unreadable file, or
|
||||
any unexpected error yields ``""`` so a misconfigured folder can never break
|
||||
a reply. Only regular ``*.md`` files are read; other files are ignored.
|
||||
"""
|
||||
directory = agents_dir or os.environ.get("AGENTS_DIR") or _DEFAULT_AGENTS_DIR
|
||||
try:
|
||||
base = Path(directory)
|
||||
if not base.is_dir():
|
||||
return ""
|
||||
parts: list[str] = []
|
||||
for path in sorted(base.glob("*.md"), key=lambda p: p.name):
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8").strip()
|
||||
except Exception:
|
||||
continue
|
||||
if text:
|
||||
parts.append(text)
|
||||
return "\n\n".join(parts).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
_SYSTEM_PROMPT_TEMPLATE: str = (
|
||||
"Persona: you are a British butler named {name} — polite, composed, quietly amused, and "
|
||||
"quietly enjoying yourself. Default voice is dry, witty, and lightly sarcastic: you notice "
|
||||
|
||||
Reference in New Issue
Block a user