feat: assemble memory context from facts and similar summaries

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:36:55 +02:00
parent 72d2bc379c
commit 33016bdf0a
2 changed files with 67 additions and 0 deletions

View File

@ -328,6 +328,28 @@ async def search_similar_conversation_summaries(query_embedding: list, exclude_c
) )
async def build_memory_context(user_message: str, conversation_id: int) -> str:
sections = []
facts = await get_all_memory_facts()
if facts:
facts_lines = "\n".join(f"- {f['content']}" for f in facts)
sections.append(f"Bekannte Fakten ueber den Nutzer:\n{facts_lines}")
try:
query_embedding = await get_embedding(user_message)
summaries = await search_similar_conversation_summaries(query_embedding, conversation_id, limit=3)
except Exception as e:
logger.warning(f"Memory retrieval skipped, embedding/search failed: {e}")
summaries = []
if summaries:
summary_lines = "\n".join(f"- {s['summary']}" for s in summaries)
sections.append(f"Relevante fruehere Gespraeche:\n{summary_lines}")
return "\n\n".join(sections)
def _caldav_calendar(): def _caldav_calendar():
dav_root = NEXTCLOUD_CALDAV_URL.split("/calendars/")[0] + "/" dav_root = NEXTCLOUD_CALDAV_URL.split("/calendars/")[0] + "/"
client = caldav.DAVClient(url=dav_root, username=NEXTCLOUD_USER, password=NEXTCLOUD_APP_PASSWORD) client = caldav.DAVClient(url=dav_root, username=NEXTCLOUD_USER, password=NEXTCLOUD_APP_PASSWORD)

View File

@ -141,3 +141,48 @@ async def test_execute_tool_dispatches_remember_and_forget_fact():
with patch.object(main, "forget_fact", new=AsyncMock(return_value={"deleted": True, "content": "X"})): with patch.object(main, "forget_fact", new=AsyncMock(return_value={"deleted": True, "content": "X"})):
result = await main.execute_tool("forget_fact", {"query": "X"}) result = await main.execute_tool("forget_fact", {"query": "X"})
assert json.loads(result) == {"deleted": True, "content": "X"} assert json.loads(result) == {"deleted": True, "content": "X"}
@pytest.mark.asyncio
async def test_build_memory_context_empty_when_nothing_stored():
with patch.object(main, "get_all_memory_facts", new=AsyncMock(return_value=[])), patch.object(
main, "get_embedding", new=AsyncMock(return_value=[0.1])
), patch.object(main, "search_similar_conversation_summaries", new=AsyncMock(return_value=[])):
context = await main.build_memory_context("Hallo", conversation_id=1)
assert context == ""
@pytest.mark.asyncio
async def test_build_memory_context_includes_facts_section():
facts = [{"id": 1, "content": "Hund heisst Bruno"}]
with patch.object(main, "get_all_memory_facts", new=AsyncMock(return_value=facts)), patch.object(
main, "get_embedding", new=AsyncMock(return_value=[0.1])
), patch.object(main, "search_similar_conversation_summaries", new=AsyncMock(return_value=[])):
context = await main.build_memory_context("Wie geht es meinem Hund?", conversation_id=1)
assert "Bekannte Fakten ueber den Nutzer" in context
assert "Hund heisst Bruno" in context
assert "Relevante fruehere Gespraeche" not in context
@pytest.mark.asyncio
async def test_build_memory_context_includes_summaries_section():
summaries = [{"conversation_id": 2, "summary": "Ging um den Kalender", "distance": 0.05}]
with patch.object(main, "get_all_memory_facts", new=AsyncMock(return_value=[])), patch.object(
main, "get_embedding", new=AsyncMock(return_value=[0.1])
), patch.object(main, "search_similar_conversation_summaries", new=AsyncMock(return_value=summaries)):
context = await main.build_memory_context("Was war nochmal mit dem Termin?", conversation_id=1)
assert "Relevante fruehere Gespraeche" in context
assert "Ging um den Kalender" in context
@pytest.mark.asyncio
async def test_build_memory_context_skips_summaries_when_embedding_fails():
with patch.object(main, "get_all_memory_facts", new=AsyncMock(return_value=[])), patch.object(
main, "get_embedding", new=AsyncMock(side_effect=RuntimeError("Ollama down"))
):
context = await main.build_memory_context("Hallo", conversation_id=1)
assert context == ""