fix: append trailing user message so conversation summary calls don't 400

Root cause: get_messages() returns history ending with the just-saved
assistant reply, but the Anthropic Messages API rejects any request whose
last message is not role=user - this failed on every single chat turn in
production, silently swallowed by the existing try/except.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V57jSQPqwkGG8BuAXg59X5
This commit is contained in:
Jonny 2026-09-13 20:58:41 +02:00
parent 246fe60ce7
commit da48882671
2 changed files with 29 additions and 0 deletions

View File

@ -1086,6 +1086,7 @@ async def update_conversation_summary(conversation_id: int):
try: try:
history = await get_messages(conversation_id) history = await get_messages(conversation_id)
claude_messages = [{"role": m["role"], "content": m["content"]} for m in history] claude_messages = [{"role": m["role"], "content": m["content"]} for m in history]
claude_messages.append({"role": "user", "content": "Fasse das bisherige Gespraech zusammen."})
completion = await asyncio.to_thread( completion = await asyncio.to_thread(
claude_client.messages.create, claude_client.messages.create,
model=CLAUDE_MODEL, model=CLAUDE_MODEL,

View File

@ -237,6 +237,34 @@ async def test_update_conversation_summary_upserts_summary_and_embedding():
mock_upsert.assert_called_once_with(3, "Nutzer fragte nach seinem Hund Bruno.", [0.1, 0.2]) mock_upsert.assert_called_once_with(3, "Nutzer fragte nach seinem Hund Bruno.", [0.1, 0.2])
@pytest.mark.asyncio
async def test_update_conversation_summary_ends_messages_with_user_role():
"""Regression: history always ends with the just-saved assistant reply,
but the Anthropic Messages API rejects a request whose last message is
not role=user (no prefill support) - a trailing user instruction must
be appended, or this call 400s on every real conversation."""
history = [
{"role": "user", "content": "Wie geht es meinem Hund?", "tokens_used": None, "created_at": None},
{"role": "assistant", "content": "Bruno geht es gut.", "tokens_used": 5, "created_at": None},
]
completion = MagicMock()
text_block = MagicMock()
text_block.type = "text"
text_block.text = "Zusammenfassung."
completion.content = [text_block]
main.claude_client = MagicMock()
main.claude_client.messages.create.return_value = completion
with patch.object(main, "get_messages", new=AsyncMock(return_value=history)), patch.object(
main, "get_embedding", new=AsyncMock(return_value=[0.1])
), patch.object(main, "upsert_conversation_summary", new=AsyncMock()):
await main.update_conversation_summary(conversation_id=3)
_, kwargs = main.claude_client.messages.create.call_args
assert kwargs["messages"][-1]["role"] == "user"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_conversation_summary_swallows_exceptions(): async def test_update_conversation_summary_swallows_exceptions():
with patch.object(main, "get_messages", new=AsyncMock(side_effect=RuntimeError("DB down"))): with patch.object(main, "get_messages", new=AsyncMock(side_effect=RuntimeError("DB down"))):