Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2d80b6885 |
@@ -51,6 +51,10 @@ def build_write_denied_paths(home: str) -> set[str]:
|
||||
os.path.join(home, ".profile"),
|
||||
os.path.join(home, ".bash_profile"),
|
||||
os.path.join(home, ".zprofile"),
|
||||
os.path.join(home, ".zshenv"),
|
||||
os.path.join(home, ".zlogin"),
|
||||
os.path.join(home, ".bash_login"),
|
||||
os.path.join(home, ".gitconfig"),
|
||||
os.path.join(home, ".netrc"),
|
||||
os.path.join(home, ".pgpass"),
|
||||
os.path.join(home, ".npmrc"),
|
||||
@@ -78,6 +82,7 @@ def build_write_denied_prefixes(home: str) -> list[str]:
|
||||
os.path.join(home, ".azure"),
|
||||
os.path.join(home, ".config", "gh"),
|
||||
os.path.join(home, ".config", "gcloud"),
|
||||
os.path.join(home, ".config", "git"),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -22,31 +22,9 @@ TitleCallback = Callable[[str], None]
|
||||
_TITLE_PROMPT = (
|
||||
"Generate a short, descriptive title (3-7 words) for a conversation that starts with the "
|
||||
"following exchange. The title should capture the main topic or intent. "
|
||||
"Write the title in the same language the user is writing in. "
|
||||
"Return ONLY the title text, nothing else. No quotes, no punctuation at the end, no prefixes."
|
||||
)
|
||||
|
||||
_TITLE_PROMPT_PINNED_LANGUAGE = (
|
||||
"Generate a short, descriptive title (3-7 words) for a conversation that starts with the "
|
||||
"following exchange. The title should capture the main topic or intent. "
|
||||
"Write the title in {language}. "
|
||||
"Return ONLY the title text, nothing else. No quotes, no punctuation at the end, no prefixes."
|
||||
)
|
||||
|
||||
|
||||
def _title_language() -> str:
|
||||
"""Return configured title language, or empty string to match the user."""
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
return str(
|
||||
((load_config() or {}).get("auxiliary") or {})
|
||||
.get("title_generation", {})
|
||||
.get("language", "")
|
||||
).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def generate_title(
|
||||
user_message: str,
|
||||
@@ -70,11 +48,8 @@ def generate_title(
|
||||
user_snippet = user_message[:500] if user_message else ""
|
||||
assistant_snippet = assistant_response[:500] if assistant_response else ""
|
||||
|
||||
language = _title_language()
|
||||
prompt = _TITLE_PROMPT_PINNED_LANGUAGE.format(language=language) if language else _TITLE_PROMPT
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "system", "content": _TITLE_PROMPT},
|
||||
{"role": "user", "content": f"User: {user_snippet}\n\nAssistant: {assistant_snippet}"},
|
||||
]
|
||||
|
||||
|
||||
@@ -1309,7 +1309,6 @@ DEFAULT_CONFIG = {
|
||||
"api_key": "",
|
||||
"timeout": 30,
|
||||
"extra_body": {},
|
||||
"language": "",
|
||||
},
|
||||
"tts_audio_tags": {
|
||||
"provider": "auto",
|
||||
|
||||
@@ -7,7 +7,6 @@ from agent.title_generator import (
|
||||
generate_title,
|
||||
auto_title_session,
|
||||
maybe_auto_title,
|
||||
_title_language,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,42 +22,6 @@ class TestGenerateTitle:
|
||||
title = generate_title("help me fix this import", "Sure, let me check...")
|
||||
assert title == "Debugging Python Import Errors"
|
||||
|
||||
def test_default_prompt_matches_user_language(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Some Title"
|
||||
|
||||
with patch("agent.title_generator.call_llm", return_value=mock_response) as llm:
|
||||
generate_title("質問です", "回答です")
|
||||
|
||||
system_prompt = llm.call_args.kwargs["messages"][0]["content"]
|
||||
assert "same language the user is writing in" in system_prompt
|
||||
|
||||
def test_configured_language_pins_prompt(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Some Title"
|
||||
|
||||
with (
|
||||
patch("agent.title_generator.call_llm", return_value=mock_response) as llm,
|
||||
patch("agent.title_generator._title_language", return_value="Japanese"),
|
||||
):
|
||||
generate_title("hello", "hi")
|
||||
|
||||
system_prompt = llm.call_args.kwargs["messages"][0]["content"]
|
||||
assert "Write the title in Japanese" in system_prompt
|
||||
assert "same language the user" not in system_prompt
|
||||
|
||||
def test_title_language_reads_config(self):
|
||||
cfg = {"auxiliary": {"title_generation": {"language": " French "}}}
|
||||
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
assert _title_language() == "French"
|
||||
with patch("hermes_cli.config.load_config", return_value={}):
|
||||
assert _title_language() == ""
|
||||
with patch("hermes_cli.config.load_config", side_effect=RuntimeError("bad config")):
|
||||
assert _title_language() == ""
|
||||
|
||||
def test_strips_quotes(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
|
||||
@@ -69,9 +69,24 @@ class TestWriteDenyExactPaths:
|
||||
|
||||
def test_shell_profiles(self):
|
||||
home = str(Path.home())
|
||||
for name in [".bashrc", ".zshrc", ".profile", ".bash_profile", ".zprofile"]:
|
||||
for name in [
|
||||
".bashrc", ".zshrc", ".profile", ".bash_profile", ".zprofile",
|
||||
".zshenv", ".zlogin", ".bash_login",
|
||||
]:
|
||||
assert _is_write_denied(os.path.join(home, name)) is True, f"{name} should be denied"
|
||||
|
||||
def test_global_git_config_paths(self):
|
||||
home = str(Path.home())
|
||||
for path in [
|
||||
os.path.join(home, ".gitconfig"),
|
||||
os.path.join(home, ".config", "git", "config"),
|
||||
os.path.join(home, ".config", "git", "hooks", "pre-commit"),
|
||||
]:
|
||||
assert _is_write_denied(path) is True, f"{path} should be denied"
|
||||
|
||||
def test_project_git_config_allowed(self):
|
||||
assert _is_write_denied("/tmp/someproject/.git/config") is False
|
||||
|
||||
def test_package_manager_configs(self):
|
||||
home = str(Path.home())
|
||||
for name in [".npmrc", ".pypirc", ".pgpass"]:
|
||||
|
||||
@@ -252,7 +252,7 @@ THREAT_PATTERNS = [
|
||||
(r'\bcrontab\b',
|
||||
"persistence_cron", "medium", "persistence",
|
||||
"modifies cron jobs"),
|
||||
(r'\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\b',
|
||||
(r'\.(bashrc|zshrc|zshenv|profile|bash_profile|bash_login|zprofile|zlogin)\b',
|
||||
"shell_rc_mod", "medium", "persistence",
|
||||
"references shell startup file"),
|
||||
(r'authorized_keys',
|
||||
|
||||
@@ -938,16 +938,6 @@ auxiliary:
|
||||
compression:
|
||||
timeout: 120 # seconds — compression summarizes long conversations, needs more time
|
||||
|
||||
# Auto-generated session titles. Empty language follows the conversation;
|
||||
# set e.g. "English" or "Japanese" to pin titles to one language.
|
||||
title_generation:
|
||||
provider: "auto"
|
||||
model: ""
|
||||
base_url: ""
|
||||
api_key: ""
|
||||
timeout: 30
|
||||
language: ""
|
||||
|
||||
# Skills hub — skill matching and search
|
||||
skills_hub:
|
||||
provider: "auto"
|
||||
|
||||
Reference in New Issue
Block a user