feat: summarize conversations in the background after each turn

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 15:40:59 +02:00
parent 2d9bf6a42b
commit c879ffc625
2 changed files with 83 additions and 0 deletions

View File

@ -783,6 +783,41 @@ async def run_chat_completion(claude_messages: list, conversation_id: int):
)
SUMMARY_SYSTEM_PROMPT = (
"Fasse das folgende Gespraech in 2-3 Saetzen auf Deutsch zusammen, damit "
"ein spaeterer Chat den Kontext wiedererkennt. Gib nur die Zusammenfassung "
"aus, ohne Einleitung."
)
async def update_conversation_summary(conversation_id: int):
try:
history = await get_messages(conversation_id)
claude_messages = [{"role": m["role"], "content": m["content"]} for m in history]
completion = await asyncio.to_thread(
claude_client.messages.create,
model=CLAUDE_MODEL,
max_tokens=200,
system=SUMMARY_SYSTEM_PROMPT,
messages=claude_messages,
)
summary_text = "".join(b.text for b in completion.content if b.type == "text")
embedding = await get_embedding(summary_text)
await upsert_conversation_summary(conversation_id, summary_text, embedding)
except Exception as e:
logger.error(f"Conversation summary update failed for conversation {conversation_id}: {e}")
_background_tasks: set = set()
def _spawn_background_task(coro):
task = asyncio.create_task(coro)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
return task
# ============ MODELS ============
class ChatRequest(BaseModel):
conversation_id: Optional[int] = None
@ -910,6 +945,8 @@ async def chat(request: ChatRequest):
await save_message(conversation_id, DEFAULT_USER_ID, "assistant", response_text, output_tokens)
_spawn_background_task(update_conversation_summary(conversation_id))
return ChatResponse(
conversation_id=conversation_id,
response=response_text,

View File

@ -212,3 +212,49 @@ async def test_run_chat_completion_includes_memory_context_in_system_prompt():
_, kwargs = main.claude_client.messages.create.call_args
assert "Hund heisst Bruno" in kwargs["system"]
assert kwargs["tools"] == main.CALENDAR_TOOLS + main.MEMORY_TOOLS
@pytest.mark.asyncio
async def test_update_conversation_summary_upserts_summary_and_embedding():
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 = "Nutzer fragte nach seinem Hund Bruno."
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, 0.2])
), patch.object(main, "upsert_conversation_summary", new=AsyncMock()) as mock_upsert:
await main.update_conversation_summary(conversation_id=3)
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_swallows_exceptions():
with patch.object(main, "get_messages", new=AsyncMock(side_effect=RuntimeError("DB down"))):
await main.update_conversation_summary(conversation_id=3)
# kein Raise - das ist der Test
def test_spawn_background_task_tracks_and_releases_task():
import asyncio as aio
async def _run():
async def noop():
return "done"
task = main._spawn_background_task(noop())
assert task in main._background_tasks
result = await task
assert result == "done"
assert task not in main._background_tasks
aio.run(_run())