|
| 1 | +--- a/agent/title_generator.py |
| 2 | ++++ b/agent/title_generator.py |
| 3 | +@@ -6,7 +6,7 @@ |
| 4 | + |
| 5 | + import logging |
| 6 | + import threading |
| 7 | +-from typing import Callable, Optional |
| 8 | ++from typing import Any, Callable, Optional |
| 9 | + |
| 10 | + from agent.auxiliary_client import call_llm |
| 11 | + |
| 12 | +@@ -26,9 +26,40 @@ |
| 13 | + ) |
| 14 | + |
| 15 | + |
| 16 | ++def _title_text(value: Any) -> str: |
| 17 | ++ """Return compact text for title prompts from string or multimodal content.""" |
| 18 | ++ if value is None: |
| 19 | ++ return "" |
| 20 | ++ if isinstance(value, str): |
| 21 | ++ return value |
| 22 | ++ if isinstance(value, list): |
| 23 | ++ parts = [] |
| 24 | ++ for item in value: |
| 25 | ++ if isinstance(item, str): |
| 26 | ++ parts.append(item) |
| 27 | ++ elif isinstance(item, dict): |
| 28 | ++ text = ( |
| 29 | ++ item.get("text") |
| 30 | ++ or item.get("summary") |
| 31 | ++ or item.get("text_summary") |
| 32 | ++ ) |
| 33 | ++ if text is not None: |
| 34 | ++ parts.append(str(text)) |
| 35 | ++ return "\n".join(part for part in parts if part) |
| 36 | ++ if isinstance(value, dict): |
| 37 | ++ text = ( |
| 38 | ++ value.get("text") |
| 39 | ++ or value.get("summary") |
| 40 | ++ or value.get("text_summary") |
| 41 | ++ ) |
| 42 | ++ if text is not None: |
| 43 | ++ return str(text) |
| 44 | ++ return str(value) |
| 45 | ++ |
| 46 | ++ |
| 47 | + def generate_title( |
| 48 | +- user_message: str, |
| 49 | +- assistant_response: str, |
| 50 | ++ user_message: Any, |
| 51 | ++ assistant_response: Any, |
| 52 | + timeout: float = 30.0, |
| 53 | + failure_callback: Optional[FailureCallback] = None, |
| 54 | + main_runtime: dict = None, |
| 55 | +@@ -45,8 +76,8 @@ |
| 56 | + of silently accumulating untitled sessions. |
| 57 | + """ |
| 58 | + # Truncate long messages to keep the request small |
| 59 | +- user_snippet = user_message[:500] if user_message else "" |
| 60 | +- assistant_snippet = assistant_response[:500] if assistant_response else "" |
| 61 | ++ user_snippet = _title_text(user_message)[:500] |
| 62 | ++ assistant_snippet = _title_text(assistant_response)[:500] |
| 63 | + |
| 64 | + messages = [ |
| 65 | + {"role": "system", "content": _TITLE_PROMPT}, |
| 66 | +@@ -87,8 +118,8 @@ |
| 67 | + def auto_title_session( |
| 68 | + session_db, |
| 69 | + session_id: str, |
| 70 | +- user_message: str, |
| 71 | +- assistant_response: str, |
| 72 | ++ user_message: Any, |
| 73 | ++ assistant_response: Any, |
| 74 | + failure_callback: Optional[FailureCallback] = None, |
| 75 | + main_runtime: dict = None, |
| 76 | + title_callback: Optional[TitleCallback] = None, |
| 77 | +@@ -133,8 +164,8 @@ |
| 78 | + def maybe_auto_title( |
| 79 | + session_db, |
| 80 | + session_id: str, |
| 81 | +- user_message: str, |
| 82 | +- assistant_response: str, |
| 83 | ++ user_message: Any, |
| 84 | ++ assistant_response: Any, |
| 85 | + conversation_history: list, |
| 86 | + failure_callback: Optional[FailureCallback] = None, |
| 87 | + main_runtime: dict = None, |
| 88 | +@@ -146,7 +177,12 @@ |
| 89 | + - This appears to be the first user→assistant exchange |
| 90 | + - No title is already set |
| 91 | + """ |
| 92 | +- if not session_db or not session_id or not user_message or not assistant_response: |
| 93 | ++ if ( |
| 94 | ++ not session_db |
| 95 | ++ or not session_id |
| 96 | ++ or not _title_text(user_message) |
| 97 | ++ or not _title_text(assistant_response) |
| 98 | ++ ): |
| 99 | + return |
| 100 | + |
| 101 | + # Count user messages in history to detect first exchange. |
| 102 | +--- a/tests/agent/test_title_generator.py |
| 103 | ++++ b/tests/agent/test_title_generator.py |
| 104 | +@@ -113,6 +113,31 @@ |
| 105 | + user_content = captured_kwargs["messages"][1]["content"] |
| 106 | + assert len(user_content) < 1100 # 500 + 500 + formatting |
| 107 | + |
| 108 | ++ def test_normalizes_none_and_multimodal_messages(self): |
| 109 | ++ # Regression: https://github.com/NousResearch/hermes-agent/issues/32883 |
| 110 | ++ # Regression: https://github.com/NousResearch/hermes-agent/issues/33041 |
| 111 | ++ captured_kwargs = {} |
| 112 | ++ |
| 113 | ++ def mock_call_llm(**kwargs): |
| 114 | ++ captured_kwargs.update(kwargs) |
| 115 | ++ resp = MagicMock() |
| 116 | ++ resp.choices = [MagicMock()] |
| 117 | ++ resp.choices[0].message.content = "Multimodal Title" |
| 118 | ++ return resp |
| 119 | ++ |
| 120 | ++ user_message = [ |
| 121 | ++ {"type": "text", "text": "Summarize this screenshot"}, |
| 122 | ++ {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}, |
| 123 | ++ ] |
| 124 | ++ |
| 125 | ++ with patch("agent.title_generator.call_llm", side_effect=mock_call_llm): |
| 126 | ++ title = generate_title(user_message, None) |
| 127 | ++ |
| 128 | ++ assert title == "Multimodal Title" |
| 129 | ++ user_content = captured_kwargs["messages"][1]["content"] |
| 130 | ++ assert "Summarize this screenshot" in user_content |
| 131 | ++ assert "Assistant: " in user_content |
| 132 | ++ |
| 133 | + |
| 134 | + class TestAutoTitleSession: |
| 135 | + """Tests for auto_title_session() — the sync worker function.""" |
0 commit comments