From cf58f1a520b177b88dbf84391eca6bc21e35b6e8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:15:52 -0700 Subject: [PATCH] feat(titles): support language-aware title generation (#45296) Make auxiliary title prompts match the user language by default, with an optional pinned `auxiliary.title_generation.language` config. --- agent/title_generator.py | 27 ++++++++++++++++- hermes_cli/config.py | 1 + tests/agent/test_title_generator.py | 37 ++++++++++++++++++++++++ website/docs/user-guide/configuration.md | 10 +++++++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/agent/title_generator.py b/agent/title_generator.py index a7f1e158e1a..583a2cfc601 100644 --- a/agent/title_generator.py +++ b/agent/title_generator.py @@ -22,9 +22,31 @@ 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, @@ -48,8 +70,11 @@ 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": _TITLE_PROMPT}, + {"role": "system", "content": prompt}, {"role": "user", "content": f"User: {user_snippet}\n\nAssistant: {assistant_snippet}"}, ] diff --git a/hermes_cli/config.py b/hermes_cli/config.py index d36f7e8a9c9..80a4bc70901 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1438,6 +1438,7 @@ DEFAULT_CONFIG = { "api_key": "", "timeout": 30, "extra_body": {}, + "language": "", }, "tts_audio_tags": { "provider": "auto", diff --git a/tests/agent/test_title_generator.py b/tests/agent/test_title_generator.py index 56286f6ecc9..43b1c1e6bf9 100644 --- a/tests/agent/test_title_generator.py +++ b/tests/agent/test_title_generator.py @@ -7,6 +7,7 @@ from agent.title_generator import ( generate_title, auto_title_session, maybe_auto_title, + _title_language, ) @@ -22,6 +23,42 @@ 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()] diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 54126817aa5..29b0ac82aae 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1007,6 +1007,16 @@ 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"