mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
Adds --persona NAME / --persona none and a [memory_files].persona_name config field, resolving ~/.openjarvis/personas/<name>/{SOUL,MEMORY,USER}.md. Default (empty) preserves today's global-persona behavior exactly. Includes a path-traversal guard on persona names. Resolution lives in SystemPromptBuilder._resolve_persona so all callers benefit. Squad-derived: Ada (Qwen3.6-27B) authored the spec independently from the issue+code; Lucy (Qwen3-Coder-Next, 80B-A3B / ~3B active) implemented it; cross-function param threading completed in the test phase. 21 existing tests pass; +8 new persona-scope tests.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
de3e86c544
commit
739bff417c
@@ -151,9 +151,7 @@ def _run_research(
|
||||
trace.print(f" [dim]↳ Found {n} {label}[/dim]")
|
||||
elif etype == "clarify_call":
|
||||
q = event.get("question", "") or ""
|
||||
trace.print(
|
||||
f" [dim]↳ Clarifying:[/dim] [dim italic]{q}[/dim italic]"
|
||||
)
|
||||
trace.print(f" [dim]↳ Clarifying:[/dim] [dim italic]{q}[/dim italic]")
|
||||
# final_answer and clarify_response are handled outside the loop.
|
||||
|
||||
agent = ResearchAgent(
|
||||
@@ -326,6 +324,7 @@ def _run_agent(
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
capability_policy=None,
|
||||
memory_files_config=None,
|
||||
):
|
||||
"""Instantiate and run an agent, returning the AgentResult."""
|
||||
# Import agents to trigger registration
|
||||
@@ -374,7 +373,7 @@ def _run_agent(
|
||||
|
||||
agent_kwargs["prompt_builder"] = SystemPromptBuilder(
|
||||
agent_template=config.agent.default_system_prompt or "",
|
||||
memory_files_config=config.memory_files,
|
||||
memory_files_config=memory_files_config or config.memory_files,
|
||||
system_prompt_config=config.system_prompt,
|
||||
)
|
||||
|
||||
@@ -592,6 +591,15 @@ def _print_profile(
|
||||
"(default: ~/.openjarvis/knowledge.db)."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--persona",
|
||||
"persona_name",
|
||||
default=None,
|
||||
help=(
|
||||
"Named persona dir under ~/.openjarvis/personas/<name>/ "
|
||||
"(overrides config). Pass 'none' to disable all persona files."
|
||||
),
|
||||
)
|
||||
@click.pass_context
|
||||
def ask(
|
||||
ctx: click.Context,
|
||||
@@ -608,6 +616,7 @@ def ask(
|
||||
enable_profile: bool,
|
||||
research_mode: bool,
|
||||
knowledge_db: str | None,
|
||||
persona_name: str | None,
|
||||
) -> None:
|
||||
"""Ask Jarvis a question."""
|
||||
quiet = (ctx.obj or {}).get("quiet", False) or output_json
|
||||
@@ -620,6 +629,15 @@ def ask(
|
||||
# Load config
|
||||
config = load_config()
|
||||
|
||||
# Resolve effective MemoryFilesConfig with --persona override
|
||||
import dataclasses as _dc
|
||||
|
||||
effective_mf = (
|
||||
_dc.replace(config.memory_files, persona_name=persona_name)
|
||||
if persona_name is not None
|
||||
else config.memory_files
|
||||
)
|
||||
|
||||
# Honor `agent.default_agent` from config when --agent was not explicitly
|
||||
# passed. Pass `--agent ""` to opt out and use direct-to-engine mode.
|
||||
# Without this fallback, `[agent].default_system_prompt` and the
|
||||
@@ -773,6 +791,7 @@ def ask(
|
||||
temperature,
|
||||
max_tokens,
|
||||
capability_policy=sec.capability_policy,
|
||||
memory_files_config=effective_mf,
|
||||
)
|
||||
except EngineConnectionError as exc:
|
||||
console.print(f"[red]Engine error:[/red] {exc}")
|
||||
|
||||
@@ -1428,6 +1428,7 @@ class MemoryFilesConfig:
|
||||
memory_path: str = "~/.openjarvis/MEMORY.md"
|
||||
user_path: str = "~/.openjarvis/USER.md"
|
||||
nudge_interval: int = 10
|
||||
persona_name: str = "" # named persona dir under ~/.openjarvis/personas/<name>/
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -35,7 +35,8 @@ class SystemPromptBuilder:
|
||||
skill_few_shot_examples: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
self._agent_template = agent_template
|
||||
self._mf_config = memory_files_config or MemoryFilesConfig()
|
||||
_mf = memory_files_config or MemoryFilesConfig()
|
||||
self._mf_config = self._resolve_persona(_mf)
|
||||
self._sp_config = system_prompt_config or SystemPromptConfig()
|
||||
self._skill_index = skill_index or []
|
||||
self._session_context = session_context
|
||||
@@ -250,3 +251,34 @@ class SystemPromptBuilder:
|
||||
+ text[-tail_size:]
|
||||
)
|
||||
return text[:max_chars] + "\n[...truncated...]"
|
||||
|
||||
@staticmethod
|
||||
def _resolve_persona(mf: MemoryFilesConfig) -> MemoryFilesConfig:
|
||||
"""Resolve persona_name to effective file paths.
|
||||
- "" (empty) -> use mf's existing paths (global default, unchanged)
|
||||
- "none" -> empty paths (opt-out, no persona injected)
|
||||
- "<name>" -> ~/.openjarvis/personas/<name>/{SOUL,MEMORY,USER}.md
|
||||
"""
|
||||
if not mf.persona_name:
|
||||
return mf
|
||||
if mf.persona_name == "none":
|
||||
return MemoryFilesConfig(
|
||||
soul_path="",
|
||||
memory_path="",
|
||||
user_path="",
|
||||
nudge_interval=mf.nudge_interval,
|
||||
)
|
||||
name = mf.persona_name
|
||||
if ".." in name or "/" in name or "\\" in name or name.startswith("/"):
|
||||
raise ValueError(
|
||||
f"Invalid persona name {name!r}: must be a simple "
|
||||
"identifier (no path separators or '..')."
|
||||
)
|
||||
base = Path.home() / ".openjarvis" / "personas" / name
|
||||
return MemoryFilesConfig(
|
||||
soul_path=str(base / "SOUL.md"),
|
||||
memory_path=str(base / "MEMORY.md"),
|
||||
user_path=str(base / "USER.md"),
|
||||
nudge_interval=mf.nudge_interval,
|
||||
persona_name=name,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests for #380 per-invocation persona scope (_resolve_persona)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openjarvis.core.config import MemoryFilesConfig
|
||||
from openjarvis.prompt.builder import SystemPromptBuilder
|
||||
|
||||
|
||||
def test_empty_persona_passes_through_global_defaults():
|
||||
mf = MemoryFilesConfig()
|
||||
out = SystemPromptBuilder._resolve_persona(mf)
|
||||
assert out.soul_path == mf.soul_path # unchanged = backward compatible
|
||||
|
||||
|
||||
def test_none_persona_disables_all_files():
|
||||
out = SystemPromptBuilder._resolve_persona(MemoryFilesConfig(persona_name="none"))
|
||||
assert out.soul_path == "" and out.memory_path == "" and out.user_path == ""
|
||||
|
||||
|
||||
def test_named_persona_resolves_to_personas_dir():
|
||||
out = SystemPromptBuilder._resolve_persona(MemoryFilesConfig(persona_name="coder"))
|
||||
base = str(Path.home() / ".openjarvis" / "personas" / "coder")
|
||||
assert out.soul_path == f"{base}/SOUL.md"
|
||||
assert out.memory_path == f"{base}/MEMORY.md"
|
||||
assert out.user_path == f"{base}/USER.md"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["../etc", "a/b", "..\\win", "/abs", "x/../y"])
|
||||
def test_path_traversal_rejected(bad):
|
||||
with pytest.raises(ValueError):
|
||||
SystemPromptBuilder._resolve_persona(MemoryFilesConfig(persona_name=bad))
|
||||
Reference in New Issue
Block a user