Skip to content

Commit 5873182

Browse files
clementb49claude
andauthored
feat: add conversation database (#1045)
* feat(convDatabase): add sqlalchemy and alembic * feat(convDatabase): implement a first version of the conversation database * chore: ignore claude code settings * refactor(convDatabase): optimize subsequent save query * feat(convDatabase): add settings and private mode * refactor(convDatabase): manage database singleton inside wx app singleton * refactor(convDatabase): optimize database manager code * fix(convDatabase): code review comment * fix(convDatabase): use correct method name * fix(convDatabase): disable btn when no conversation history is selected * fix(build_exe): add missing package for sqlalchemy and alembic * feat: move alembic version in resource dir to simplify fhrozen version * fix(frozenAPP): move alembic dir in resource * style: fix docstring * fix: address review findings across database, GUI, and test layers - gitignore: remove personal .claude/settings.local.json entry (advise contributors to add it to their global gitignore instead) - models: use timezone-aware datetime.now(timezone.utc) for all created_at/updated_at column defaults and onupdate callables - models: add ondelete="SET NULL" to conversation_system_prompt_id FK to avoid constraint violations during cascaded deletes - conversation_model: add SystemMessage.__eq__ that compares only role and content (mirrors __hash__), fixing lookup in OrderedSet when db_id differs - manager: add ConversationDatabase.from_engine() factory classmethod for test-friendly initialisation without running Alembic migrations - main_app: fix docstring typo ("started the background" → "started in the background"); wrap init_conversation_db in try/except; capitalise log messages in close_conversation_db - main_frame: guard on_close against empty tabs_panels / invalid index before calling current_tab - conversation_tab: remove stale _conv_db class-level cache; always return wx.GetApp().conv_db directly; add DRAFT_SAVE_DELAY_MS constant; extract _pop_draft_if_present helper; fix set_private to only clear db_conv_id on successful delete; make _build_draft_block accept optional prompt_text/attachments args; pass pre-fetched values from _save_draft_to_db to avoid double-read - conversation_history_dialog: replace hardcoded limit=200 with PAGE_SIZE=100 constant; add offset/reset pagination with Load More button; add count_label showing "Showing N of M"; fix _on_item_deselected to use wx.CallAfter(_update_buttons_state) - preferences_dialog: disable auto_save_draft when auto_save_to_db is unchecked; add on_auto_save_to_db_changed EVT_CHECKBOX handler - pyproject.toml: sort alembic and sqlalchemy into correct alphabetical position in dependencies - tests/conftest: use ConversationDatabase.from_engine() instead of __new__ + manual attribute injection - tests/test_manager: fix flaky test_list_ordered_by_updated with deterministic updated_at values via direct DB update - tests/test_models: add db_session.rollback() after every pytest.raises(IntegrityError) block - tests/test_roundtrip: verify restored attachment content matches original in test_attachment_dedup_roundtrip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(database): add cleanup_orphan_attachments to reclaim orphaned blobs DBAttachment uses content-hash deduplication and has no cascade delete from DBMessageAttachment, so blob rows can accumulate as orphans when conversations or draft blocks are deleted. Add ConversationDatabase.cleanup_orphan_attachments() that identifies unreferenced rows via a LEFT JOIN on DBMessageAttachment and removes them in a single transaction. Wire it to: - __init__ (once at startup, to sweep orphans from previous sessions) - delete_conversation (after each cascade delete commits) Add TestCleanupOrphanAttachments covering: no-orphan path, full cleanup after a conversation delete, and shared-attachment survival when only one of two referencing conversations is removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(conversation): reduce setup duplication across database and conversation tests - Add db_message_block fixture to database conftest; drop redundant created_at/updated_at kwargs from all DBConversation/DBMessageBlock constructions in test_models.py - Replace _make_block, _make_message_and_attachment, _make_message class helpers with the shared db_message_block fixture - Lift local imports and add _count_attachments helper in test_manager.py - Add tests/conversation/conftest.py with shared bskc_path and storage_path fixtures; remove duplicate definitions from 3 test classes - Replace 6 numbered fixtures with make_user_block/make_system factories in TestConversationWithMultipleBlocks - Move import io and from PIL import Image to module level in test_migration.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: save draft prompt after model and account selection * chore: remove claude file * fix: flush the draft before closing tab --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1406463 commit 5873182

26 files changed

+3596
-115
lines changed

basilisk/config/main_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class ConversationSettings(BaseModel):
4545
shift_enter_mode: bool = Field(default=False)
4646
use_accessible_output: bool = Field(default=True)
4747
focus_history_after_send: bool = Field(default=False)
48+
auto_save_to_db: bool = Field(default=True)
49+
auto_save_draft: bool = Field(default=True)
50+
reopen_last_conversation: bool = Field(default=False)
51+
last_active_conversation_id: int | None = Field(default=None)
4852

4953

5054
class ImagesSettings(BaseModel):

basilisk/conversation/attached_file.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ class AttachmentFile(BaseModel):
226226
description: str | None = None
227227
size: int | None = None
228228
mime_type: str | None = None
229+
db_id: int | None = Field(default=None, exclude=True)
229230

230231
@field_serializer("location", mode="wrap")
231232
@classmethod

basilisk/conversation/conversation_model.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class SystemMessage(BaseMessage):
9090
"""Represents a system message in a conversation. The system message is used to provide instructions or context to the assistant."""
9191

9292
role: MessageRoleEnum = Field(default=MessageRoleEnum.SYSTEM)
93+
db_id: int | None = Field(default=None, exclude=True)
9394

9495
@field_validator("role", mode="after")
9596
@classmethod
@@ -117,6 +118,19 @@ def __hash__(self):
117118
"""
118119
return hash((self.role, self.content))
119120

121+
def __eq__(self, other: object) -> bool:
122+
"""Compare system messages by role and content, ignoring db_id.
123+
124+
Args:
125+
other: The object to compare against.
126+
127+
Returns:
128+
True if role and content match, False otherwise.
129+
"""
130+
if not isinstance(other, SystemMessage):
131+
return NotImplemented
132+
return self.role == other.role and self.content == other.content
133+
120134

121135
class MessageBlock(BaseModel):
122136
"""Represents a block of messages in a conversation. The block may contain a user message, an AI model request, and an AI model response."""
@@ -131,6 +145,7 @@ class MessageBlock(BaseModel):
131145
stream: bool = Field(default=False)
132146
created_at: datetime = Field(default_factory=datetime.now)
133147
updated_at: datetime = Field(default_factory=datetime.now)
148+
db_id: int | None = Field(default=None, exclude=True)
134149

135150
@field_validator("response", mode="after")
136151
@classmethod
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Database package for conversation persistence."""
2+
3+
from .manager import ConversationDatabase
4+
5+
__all__ = ["ConversationDatabase"]

0 commit comments

Comments
 (0)