186 lines
6.9 KiB
Python
186 lines
6.9 KiB
Python
from pathlib import Path
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from backend.chat_memory import (
|
|
build_chat_memory_context,
|
|
ensure_chat_memory_schema,
|
|
sync_all_chat_memory,
|
|
sync_chat_memory_for_session,
|
|
)
|
|
from backend.database import Base
|
|
from backend.models import ChatMessage, ChatSession
|
|
|
|
|
|
class ChatMemoryTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.engine = create_engine(
|
|
f"sqlite:///{Path(self.temp.name) / 'memory.db'}",
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
Base.metadata.create_all(self.engine)
|
|
ensure_chat_memory_schema(self.engine)
|
|
self.Session = sessionmaker(bind=self.engine)
|
|
|
|
def tearDown(self):
|
|
self.engine.dispose()
|
|
self.temp.cleanup()
|
|
|
|
def _turn(self, db, session_id, title, user_content, assistant_content, created_at=None):
|
|
created_at = created_at or datetime.utcnow()
|
|
session = ChatSession(session_id=session_id, name=title, created_at=created_at)
|
|
db.add(session)
|
|
db.flush()
|
|
db.add(ChatMessage(session_pk=session.id, role="user", content=user_content, created_at=created_at))
|
|
db.flush()
|
|
db.add(ChatMessage(session_pk=session.id, role="assistant", content=assistant_content, created_at=created_at + timedelta(seconds=1)))
|
|
db.flush()
|
|
return session
|
|
|
|
def _turn_with_sources(self, db, session_id, title, user_content, assistant_content, sources):
|
|
session = ChatSession(session_id=session_id, name=title)
|
|
db.add(session)
|
|
db.flush()
|
|
db.add(ChatMessage(session_pk=session.id, role="user", content=user_content))
|
|
db.flush()
|
|
db.add(ChatMessage(
|
|
session_pk=session.id,
|
|
role="assistant",
|
|
content=assistant_content,
|
|
sources_json=json.dumps(sources),
|
|
))
|
|
db.flush()
|
|
return session
|
|
|
|
def test_retrieves_completed_past_turns(self):
|
|
db = self.Session()
|
|
try:
|
|
self._turn(
|
|
db,
|
|
"past",
|
|
"Tauri Packaging",
|
|
"What did we decide about mac packaging?",
|
|
"Use the PyInstaller backend sidecar and keep app data outside the bundle.",
|
|
)
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
context = build_chat_memory_context(db, "remind me about the backend sidecar packaging plan")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertIn("chat_memory_context", context["context_block"])
|
|
self.assertIn("PyInstaller backend sidecar", context["context_block"])
|
|
self.assertEqual(context["sources"][0]["source_session_id"], "past")
|
|
|
|
def test_excludes_active_session(self):
|
|
db = self.Session()
|
|
try:
|
|
self._turn(db, "current", "Current", "secret kiwi topic", "Only the current chat mentions kiwi.")
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
excluded = build_chat_memory_context(db, "kiwi topic", exclude_session_id="current")
|
|
included = build_chat_memory_context(db, "kiwi topic")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertEqual(excluded["context_block"], "")
|
|
self.assertIn("kiwi", included["context_block"])
|
|
|
|
def test_resync_removes_stale_turn_content(self):
|
|
db = self.Session()
|
|
try:
|
|
session = self._turn(db, "past", "Old Decision", "deployment detail", "Keep using the banana corridor plan.")
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
assistant = db.query(ChatMessage).filter(ChatMessage.role == "assistant").one()
|
|
assistant.content = "This turn now discusses an unrelated renderer detail."
|
|
db.commit()
|
|
sync_chat_memory_for_session(db, session.session_id)
|
|
|
|
context = build_chat_memory_context(db, "banana corridor plan")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertEqual(context["context_block"], "")
|
|
|
|
def test_generic_prompt_words_do_not_create_false_memory_hits(self):
|
|
db = self.Session()
|
|
try:
|
|
self._turn(
|
|
db,
|
|
"story",
|
|
"Test Conversation Start",
|
|
"tell me a long story",
|
|
"The village of Eldoria sat beneath a whispering forest canopy.",
|
|
)
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
context = build_chat_memory_context(db, "tell me about thelama and crowley")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertEqual(context["context_block"], "")
|
|
|
|
def test_previous_conversation_uses_recency_not_semantic_overlap(self):
|
|
db = self.Session()
|
|
try:
|
|
base = datetime(2026, 1, 1, 12, 0, 0)
|
|
self._turn(db, "older", "Weather", "what is the weather?", "It is rainy.", created_at=base)
|
|
self._turn(db, "previous", "Crowley", "tell me about Crowley", "The previous chat discussed Crowley.", created_at=base + timedelta(minutes=1))
|
|
current = ChatSession(session_id="current", name="Letztes Gespräch", created_at=base + timedelta(minutes=2))
|
|
db.add(current)
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
context = build_chat_memory_context(db, "worum ging es im letzten gespräch?", exclude_session_id="current")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertIn("Crowley", context["context_block"])
|
|
self.assertNotIn("rainy", context["context_block"])
|
|
|
|
def test_music_query_matches_song_and_lied_history(self):
|
|
db = self.Session()
|
|
try:
|
|
self._turn(db, "song", "Identify this song", "what is this song?", "We discussed a track by an electronic artist.")
|
|
self._turn(db, "lied", "Lied: Hintergründe suchen", "suche hintergründe zu diesem lied", "Das Lied wurde im Kontext einer Band besprochen.")
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
context = build_chat_memory_context(db, "list all the music we talked about")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertIn("song", context["context_block"].casefold())
|
|
self.assertIn("lied", context["context_block"].casefold())
|
|
|
|
def test_memory_only_answers_are_not_reindexed(self):
|
|
db = self.Session()
|
|
try:
|
|
self._turn_with_sources(
|
|
db,
|
|
"poison",
|
|
"Backfire Basics",
|
|
"erzähl mir über backfire basics",
|
|
"Backfire Basics is a hallucinated project description.",
|
|
[{"type": "chat_memory", "source_session_id": "older", "source_message_id": "m1"}],
|
|
)
|
|
db.commit()
|
|
sync_all_chat_memory(db)
|
|
|
|
context = build_chat_memory_context(db, "backfire basics")
|
|
finally:
|
|
db.close()
|
|
|
|
self.assertEqual(context["context_block"], "")
|