diff --git a/Claude outputs/main.py b/Claude outputs/main.py index ac4bcc3..357951e 100644 --- a/Claude outputs/main.py +++ b/Claude outputs/main.py @@ -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, diff --git a/Claude outputs/tests/test_memory.py b/Claude outputs/tests/test_memory.py index e260f3f..8e911bc 100644 --- a/Claude outputs/tests/test_memory.py +++ b/Claude outputs/tests/test_memory.py @@ -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())