Compare commits

...

1 Commits

Author SHA1 Message Date
Teknium
8167238771 feat(titles): language-aware session titles with optional pinned language
Inspired by Claude Code v2.1.176 (June 2026): session titles are
generated in the conversation's language by default, and
auxiliary.title_generation.language pins a specific one.

- title_generator.py: default prompt now instructs the model to match
  the user's language (was unspecified, biasing titles to English);
  a pinned language from config swaps in an override prompt
- config.py: language key added to title_generation defaults
- docs: title_generation block added to the auxiliary config reference
2026-06-12 17:16:46 -07:00
4 changed files with 82 additions and 1 deletions

View File

@@ -22,9 +22,34 @@ 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."
)
# When the user pins a title language in config.yaml
# (auxiliary.title_generation.language), this replaces the
# match-the-conversation instruction. Inspired by Claude Code v2.1.176
# (June 2026): "Session titles now generated in the conversation's
# language (`language` setting pins a specific one)."
_TITLE_PROMPT_PINNED = (
"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}, regardless of the conversation's language. "
"Return ONLY the title text, nothing else. No quotes, no punctuation at the end, no prefixes."
)
def _pinned_title_language() -> str:
"""Return the configured title language, or '' to match the conversation."""
try:
from hermes_cli.config import load_config
aux = (load_config() or {}).get("auxiliary") or {}
task_cfg = aux.get("title_generation") or {}
return str(task_cfg.get("language") or "").strip()
except Exception:
return ""
def generate_title(
user_message: str,
@@ -48,8 +73,13 @@ def generate_title(
user_snippet = user_message[:500] if user_message else ""
assistant_snippet = assistant_response[:500] if assistant_response else ""
pinned = _pinned_title_language()
system_prompt = (
_TITLE_PROMPT_PINNED.format(language=pinned) if pinned else _TITLE_PROMPT
)
messages = [
{"role": "system", "content": _TITLE_PROMPT},
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"User: {user_snippet}\n\nAssistant: {assistant_snippet}"},
]

View File

@@ -1309,6 +1309,10 @@ DEFAULT_CONFIG = {
"api_key": "",
"timeout": 30,
"extra_body": {},
# Pin session titles to a specific language (e.g. "English",
# "Japanese"). Empty = match the conversation's language.
# Inspired by Claude Code v2.1.176's `language` setting.
"language": "",
},
"tts_audio_tags": {
"provider": "auto",

View File

@@ -22,6 +22,43 @@ 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_conversation_language(self):
"""Without a pinned language, the prompt asks to match the user's language."""
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._pinned_title_language", return_value=""):
generate_title("質問です", "回答です")
system_msg = llm.call_args.kwargs["messages"][0]["content"]
assert "same language the user is writing in" in system_msg
def test_pinned_language_overrides_prompt(self):
"""auxiliary.title_generation.language pins the title language."""
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._pinned_title_language", return_value="English"):
generate_title("質問です", "回答です")
system_msg = llm.call_args.kwargs["messages"][0]["content"]
assert "Write the title in English" in system_msg
assert "regardless of the conversation's language" in system_msg
def test_pinned_language_config_read(self):
"""_pinned_title_language reads auxiliary.title_generation.language."""
from agent.title_generator import _pinned_title_language
cfg = {"auxiliary": {"title_generation": {"language": " French "}}}
with patch("hermes_cli.config.load_config", return_value=cfg):
assert _pinned_title_language() == "French"
with patch("hermes_cli.config.load_config", return_value={}):
assert _pinned_title_language() == ""
with patch("hermes_cli.config.load_config", side_effect=RuntimeError):
assert _pinned_title_language() == ""
def test_strips_quotes(self):
mock_response = MagicMock()
mock_response.choices = [MagicMock()]

View File

@@ -938,6 +938,16 @@ auxiliary:
compression:
timeout: 120 # seconds — compression summarizes long conversations, needs more time
# Auto-generated session titles (first exchange of each session)
title_generation:
provider: "auto"
model: "" # cheap/fast model recommended (e.g. gemini-flash, haiku)
base_url: ""
api_key: ""
timeout: 30
language: "" # pin titles to a language, e.g. "English" or "Japanese";
# empty = title follows the conversation's language
# Skills hub — skill matching and search
skills_hub:
provider: "auto"