mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-15 12:10:19 +00:00
163df17410
## Release v0.51.262 — Release ID (stage-r12) Phase-3 light slice (no-screenshot items) — 3 PRs. ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3432 (#3532) | @franksong2702 | Normalize the recall-prefill terminal `user` turn so WebUI doesn't send adjacent `user` roles to strict chat templates (Mistral/Gemma/Jinja). `_normalize_prefill_messages_before_user_turn()` in both `streaming.py` + `gateway_chat.py`; drops only the terminal user tail, preserves assistant/system/mid-list context. **Rebased onto master** (was CONFLICTING). | | #2558 (#3516) | @rodboev | "Reveal in file manager" now translates container workspace paths (`/workspace`) back to the host mount path for Docker deployments (traversal-safe via `safe_resolve` + sibling-prefix guard). | ### Changed | Issue/PR | Author | Change | |----------|--------|--------| | (#3539) | @Lyr-GW | Completed the Chinese (Simplified) `zh` localization (MCP controls, tool-list pagination) with all interpolations preserved, and the language dropdown now applies the locale **instantly** on change. | ### Review fix absorbed (Codex) #3539 also added an `allowed=['en','zh']` filter to the Settings language dropdown, which dropped the 9 other shipped locales (it/ja/ru/es/de/pt/ko/fr/tr) — and since save falls back to `en` when the select has no matching option, an existing user of those locales would be **silently reset to English** on a Settings save. Removed the filter (dropdown enumerates all `LOCALES` again, matching master); partially-translated locales fall back per-key to English at render. + regression test `test_issue3539_language_dropdown_all_locales.py`. ### Gate - Full pytest suite: **7701 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (dropdown drops-locales) → fixed → **SAFE TO SHIP** (verified prefill drops only terminal user tail in both paths, Docker path-translation traversal-safe, zh interpolations preserved) Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: Lyr-GW <Lyr-GW@users.noreply.github.com>
178 lines
6.4 KiB
Python
178 lines
6.4 KiB
Python
import logging
|
|
|
|
from api.streaming import (
|
|
_normalize_prefill_messages_before_user_turn,
|
|
_prefill_messages_with_webui_context,
|
|
_webui_ephemeral_system_prompt,
|
|
)
|
|
|
|
|
|
def test_webui_ephemeral_prompt_includes_browser_surface_context():
|
|
prompt = _webui_ephemeral_system_prompt(
|
|
"Use a concise tone.",
|
|
surface_context={
|
|
"source": "webui",
|
|
"session_id": "session-123",
|
|
"profile": "default",
|
|
"workspace": "/tmp/example-workspace",
|
|
},
|
|
)
|
|
|
|
assert "Use a concise tone." in prompt
|
|
assert "WebUI session context" in prompt
|
|
assert "Source: webui" in prompt
|
|
assert "Session ID: session-123" in prompt
|
|
assert "Profile: default" in prompt
|
|
assert "Workspace: /tmp/example-workspace" in prompt
|
|
assert "not the same live transcript as Telegram" in prompt
|
|
assert "Do not copy or dump this browser transcript" in prompt
|
|
assert "Write to external notes or durable memory only" in prompt
|
|
assert "otherwise leave notes unchanged" in prompt
|
|
assert "what note/section changed" in prompt
|
|
assert "explicit captures" in prompt
|
|
assert "durable user preferences" in prompt
|
|
assert "Do not include terse planning fragments" in prompt
|
|
assert "Final visible assistant replies" in prompt
|
|
assert "user-facing English" not in prompt
|
|
assert "in the user's language" in prompt
|
|
assert "Need script" in prompt
|
|
assert "Need inspect email" in prompt
|
|
assert "clear user-facing progress" in prompt
|
|
|
|
|
|
def test_webui_ephemeral_prompt_skips_empty_surface_fields():
|
|
prompt = _webui_ephemeral_system_prompt(
|
|
None,
|
|
surface_context={
|
|
"source": "webui",
|
|
"session_id": "",
|
|
"profile": None,
|
|
"workspace": " ",
|
|
},
|
|
)
|
|
|
|
assert "WebUI session context" in prompt
|
|
assert "Source: webui" in prompt
|
|
assert "Session ID:" not in prompt
|
|
assert "Profile:" not in prompt
|
|
assert "Workspace:" not in prompt
|
|
|
|
|
|
def test_ephemeral_prompt_avoids_platform_info_when_no_config():
|
|
"""Without config_data, the delivery context falls back to defaults."""
|
|
prompt = _webui_ephemeral_system_prompt(
|
|
"Be concise.",
|
|
surface_context={"source": "webui"},
|
|
)
|
|
|
|
# Core platform headings should still appear (fallback data).
|
|
assert "Connected Platforms:" in prompt
|
|
assert "Delivery options for scheduled tasks:" in prompt
|
|
# But home channels are only present when the config has them.
|
|
assert "Home Channels" not in prompt
|
|
|
|
|
|
def test_prefill_no_longer_adds_session_context_user_message():
|
|
"""_prefill_messages_with_webui_context must NOT append a user message.
|
|
|
|
Strict chat templates (Mistral, Gemma) require user/assistant alternation.
|
|
Adding a 'user' session context message creates two consecutive user turns.
|
|
"""
|
|
prefill = {"messages": [{"role": "system", "content": "recall note"}]}
|
|
result = _prefill_messages_with_webui_context(prefill)
|
|
assert len(result) == 1
|
|
assert result[0]["role"] == "system"
|
|
assert "Connected Platforms" not in result[0].get("content", "")
|
|
|
|
|
|
def test_prefill_boundary_normalizer_removes_terminal_user_tail():
|
|
"""Trailing user messages are removed until the first non-user boundary."""
|
|
|
|
raw = [
|
|
{"role": "assistant", "content": "recall summary"},
|
|
{"role": "user", "content": "session context"},
|
|
]
|
|
|
|
assert _normalize_prefill_messages_before_user_turn(raw) == [
|
|
{"role": "assistant", "content": "recall summary"},
|
|
]
|
|
assert _normalize_prefill_messages_before_user_turn([
|
|
{"role": "assistant", "content": "prefill"},
|
|
{"role": "user", "content": "legacy user"},
|
|
{"role": "assistant", "content": "assistant follow-up"},
|
|
]) == [
|
|
{"role": "assistant", "content": "prefill"},
|
|
{"role": "user", "content": "legacy user"},
|
|
{"role": "assistant", "content": "assistant follow-up"},
|
|
]
|
|
assert _normalize_prefill_messages_before_user_turn([
|
|
{"role": "assistant", "content": "tail",},
|
|
{"role": "user", "content": "first"},
|
|
{"role": "user", "content": "second"},
|
|
]) == [
|
|
{"role": "assistant", "content": "tail",},
|
|
]
|
|
assert _normalize_prefill_messages_before_user_turn([]) == []
|
|
assert _normalize_prefill_messages_before_user_turn([{"role": "user", "content": "only user"}]) == []
|
|
|
|
|
|
def test_prefill_boundary_normalizer_logs_when_user_tail_dropped(caplog):
|
|
"""Dropping trailing user messages is logged with the count."""
|
|
caplog.set_level(logging.DEBUG, logger="api.streaming")
|
|
|
|
messages = [
|
|
{"role": "assistant", "content": "prefill"},
|
|
{"role": "user", "content": "first"},
|
|
{"role": "user", "content": "second"},
|
|
]
|
|
|
|
assert _normalize_prefill_messages_before_user_turn(messages) == [
|
|
{"role": "assistant", "content": "prefill"},
|
|
]
|
|
assert any(
|
|
rec.message == "Dropped 2 trailing user message(s) from prefill" for rec in caplog.records
|
|
)
|
|
|
|
|
|
def test_prefill_boundary_normalizer_no_log_when_no_terminal_user(caplog):
|
|
"""No-op normalization must not emit the prefill-dropping debug log."""
|
|
caplog.set_level(logging.DEBUG, logger="api.streaming")
|
|
|
|
messages = [
|
|
{"role": "assistant", "content": "turn 1"},
|
|
{"role": "user", "content": "turn 2"},
|
|
{"role": "assistant", "content": "turn 3"},
|
|
]
|
|
|
|
assert _normalize_prefill_messages_before_user_turn(messages) == messages
|
|
assert len(caplog.records) == 0
|
|
|
|
|
|
def test_prefill_preserves_empty_and_none_messages():
|
|
"""Edge cases: empty prefill stays empty, missing key returns empty."""
|
|
assert _prefill_messages_with_webui_context({"messages": []}) == []
|
|
assert _prefill_messages_with_webui_context({}) == []
|
|
assert _prefill_messages_with_webui_context({"messages": None}) == []
|
|
|
|
|
|
def test_delivery_context_includes_home_channels_when_configured():
|
|
"""When config_data has platforms with a home_channel, the prompt includes it."""
|
|
config = {
|
|
"platforms": {
|
|
"telegram": {
|
|
"enabled": True,
|
|
"home_channel": {"name": "General"},
|
|
},
|
|
},
|
|
}
|
|
prompt = _webui_ephemeral_system_prompt(
|
|
None,
|
|
surface_context={"source": "webui"},
|
|
config_data=config,
|
|
)
|
|
|
|
assert "Connected Platforms:" in prompt
|
|
assert "Home Channels (default destinations):" in prompt
|
|
assert "telegram: General" in prompt
|
|
assert "telegram" in prompt and "Home channel" in prompt
|