Files
Atomizer/atomizer-dashboard/backend/api/services/conversation_store.py
Anto01 73a7b9d9f1 feat: Add dashboard chat integration and MCP server
Major changes:
- Dashboard: WebSocket-based chat with session management
- Dashboard: New chat components (ChatPane, ChatInput, ModeToggle)
- Dashboard: Enhanced UI with parallel coordinates chart
- MCP Server: New atomizer-tools server for Claude integration
- Extractors: Enhanced Zernike OPD extractor
- Reports: Improved report generator

New studies (configs and scripts only):
- M1 Mirror: Cost reduction campaign studies
- Simple Beam, Simple Bracket, UAV Arm studies

Note: Large iteration data (2_iterations/, best_design_archive/)
excluded via .gitignore - kept on local Gitea only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:53:55 -05:00

296 lines
8.9 KiB
Python

"""
Conversation Store
SQLite-backed persistence for Claude chat sessions.
"""
import json
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
# Database path relative to backend directory
DB_PATH = Path(__file__).parent.parent.parent / "sessions.db"
class ConversationStore:
"""SQLite-backed conversation storage for Claude sessions"""
def __init__(self, db_path: Path = DB_PATH):
self.db_path = db_path
self._init_db()
def _get_conn(self) -> sqlite3.Connection:
"""Get database connection with row factory"""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init_db(self):
"""Initialize database schema"""
conn = self._get_conn()
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
mode TEXT NOT NULL DEFAULT 'user',
study_id TEXT,
created_at TEXT NOT NULL,
last_active TEXT NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
tool_calls TEXT,
timestamp TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_messages_session
ON messages(session_id)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_messages_timestamp
ON messages(session_id, timestamp)
""")
conn.commit()
finally:
conn.close()
def create_session(
self,
session_id: str,
mode: str = "user",
study_id: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create a new session record.
Args:
session_id: Unique session identifier
mode: "user" or "power"
study_id: Optional study context
Returns:
Session record dict
"""
conn = self._get_conn()
try:
now = datetime.now().isoformat()
conn.execute(
"""INSERT INTO sessions (session_id, mode, study_id, created_at, last_active)
VALUES (?, ?, ?, ?, ?)""",
(session_id, mode, study_id, now, now),
)
conn.commit()
return {
"session_id": session_id,
"mode": mode,
"study_id": study_id,
"created_at": now,
"last_active": now,
}
finally:
conn.close()
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Get session by ID"""
conn = self._get_conn()
try:
cursor = conn.execute(
"SELECT * FROM sessions WHERE session_id = ?",
(session_id,),
)
row = cursor.fetchone()
if row:
return dict(row)
return None
finally:
conn.close()
def update_session(
self,
session_id: str,
mode: Optional[str] = None,
study_id: Optional[str] = None,
):
"""Update session properties"""
conn = self._get_conn()
try:
updates = ["last_active = ?"]
values: List[Any] = [datetime.now().isoformat()]
if mode is not None:
updates.append("mode = ?")
values.append(mode)
if study_id is not None:
updates.append("study_id = ?")
values.append(study_id)
values.append(session_id)
conn.execute(
f"UPDATE sessions SET {', '.join(updates)} WHERE session_id = ?",
values,
)
conn.commit()
finally:
conn.close()
def touch_session(self, session_id: str):
"""Update last_active timestamp"""
conn = self._get_conn()
try:
conn.execute(
"UPDATE sessions SET last_active = ? WHERE session_id = ?",
(datetime.now().isoformat(), session_id),
)
conn.commit()
finally:
conn.close()
def add_message(
self,
session_id: str,
role: str,
content: str,
tool_calls: Optional[List[Dict]] = None,
) -> int:
"""
Add a message to a session.
Args:
session_id: Session identifier
role: "user" or "assistant"
content: Message text content
tool_calls: Optional list of tool call records
Returns:
Message ID
"""
conn = self._get_conn()
try:
cursor = conn.execute(
"""INSERT INTO messages (session_id, role, content, tool_calls, timestamp)
VALUES (?, ?, ?, ?, ?)""",
(
session_id,
role,
content,
json.dumps(tool_calls) if tool_calls else None,
datetime.now().isoformat(),
),
)
# Update session last_active
conn.execute(
"UPDATE sessions SET last_active = ? WHERE session_id = ?",
(datetime.now().isoformat(), session_id),
)
conn.commit()
return cursor.lastrowid or 0
finally:
conn.close()
def get_history(
self,
session_id: str,
limit: int = 50,
offset: int = 0,
) -> List[Dict[str, Any]]:
"""
Get conversation history for a session.
Args:
session_id: Session identifier
limit: Maximum messages to return
offset: Number of messages to skip (from most recent)
Returns:
List of message dicts in chronological order
"""
conn = self._get_conn()
try:
cursor = conn.execute(
"""
SELECT role, content, tool_calls, timestamp
FROM messages
WHERE session_id = ?
ORDER BY id DESC
LIMIT ? OFFSET ?
""",
(session_id, limit, offset),
)
rows = cursor.fetchall()
messages = []
for row in reversed(rows):
msg: Dict[str, Any] = {
"role": row["role"],
"content": row["content"],
"timestamp": row["timestamp"],
}
if row["tool_calls"]:
msg["tool_calls"] = json.loads(row["tool_calls"])
messages.append(msg)
return messages
finally:
conn.close()
def get_message_count(self, session_id: str) -> int:
"""Get total message count for a session"""
conn = self._get_conn()
try:
cursor = conn.execute(
"SELECT COUNT(*) FROM messages WHERE session_id = ?",
(session_id,),
)
return cursor.fetchone()[0]
finally:
conn.close()
def delete_session(self, session_id: str):
"""Delete a session and all its messages"""
conn = self._get_conn()
try:
conn.execute(
"DELETE FROM messages WHERE session_id = ?",
(session_id,),
)
conn.execute(
"DELETE FROM sessions WHERE session_id = ?",
(session_id,),
)
conn.commit()
finally:
conn.close()
def get_stale_sessions(self, max_age_hours: int = 24) -> List[str]:
"""Get session IDs that haven't been active for max_age_hours"""
conn = self._get_conn()
try:
cutoff = datetime.now()
# Calculate cutoff time
from datetime import timedelta
cutoff = (cutoff - timedelta(hours=max_age_hours)).isoformat()
cursor = conn.execute(
"SELECT session_id FROM sessions WHERE last_active < ?",
(cutoff,),
)
return [row["session_id"] for row in cursor.fetchall()]
finally:
conn.close()
def cleanup_stale_sessions(self, max_age_hours: int = 24) -> int:
"""Delete stale sessions and return count deleted"""
stale = self.get_stale_sessions(max_age_hours)
for session_id in stale:
self.delete_session(session_id)
return len(stale)