Skip to content

Commit dabc082

Browse files
committed
fix(hermes): normalize title generation inputs
1 parent d2ca277 commit dabc082

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

overlays/hermes-agent/default.nix

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
_final: prev:
2+
3+
{
4+
llm-agents = (prev.llm-agents or { }) // {
5+
"hermes-agent" = prev.llm-agents."hermes-agent".overrideAttrs (old: {
6+
patches = (old.patches or [ ]) ++ [
7+
../../patches/hermes-agent/0002-normalize-auto-title-inputs.patch
8+
];
9+
});
10+
};
11+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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

Comments
 (0)