feat chat history

This commit is contained in:
fsylvain committed 2026-09-19 23:02:04 +02:00
1 parent 48c9456223
commit 1648b44189
2 files changed
+250 -2

No files matched your search

+78
View File
@@ -2,7 +2,10 @@ import json
import os
import re
import time
import uuid
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
# CUDA 13.x moved its runtime DLLs to bin/x64, but llama_cpp's loader only
@@ -105,6 +108,81 @@ def extract_tool_calls(text: str) -> list[dict]:
return calls
# Chat sessions are persisted outside the repo, one JSON file per session, so
# history survives closing the app and isn't lost on an app update/reinstall.
CHATS_DIR = Path.home() / ".dev_assist_chatbot" / "chats"
# Placeholder title for a session that hasn't received its first message yet;
# used to detect when a title still needs to be derived from that message.
DEFAULT_TITLE = "New chat"
@dataclass
class ChatSession:
id: str
title: str
created_at: str
updated_at: str
history: list[tuple[str, str]] = field(default_factory=list)
def new_session() -> ChatSession:
now = datetime.now(timezone.utc).isoformat()
return ChatSession(id=uuid.uuid4().hex, title=DEFAULT_TITLE, created_at=now, updated_at=now)
def derive_title(question: str, max_len: int = 40) -> str:
"""First line of a session's opening question, trimmed to max_len - used
as its display title once it gets its first message."""
title = question.strip().splitlines()[0]
return title if len(title) <= max_len else title[: max_len - 1].rstrip() + "…"
def _session_path(session_id: str) -> Path:
return CHATS_DIR / f"{session_id}.json"
def save_session(session: ChatSession) -> None:
session.updated_at = datetime.now(timezone.utc).isoformat()
CHATS_DIR.mkdir(parents=True, exist_ok=True)
payload = {
"id": session.id,
"title": session.title,
"created_at": session.created_at,
"updated_at": session.updated_at,
"history": [[question, answer] for question, answer in session.history],
}
_session_path(session.id).write_text(json.dumps(payload, indent=2), encoding="utf-8")
def delete_session(session_id: str) -> None:
_session_path(session_id).unlink(missing_ok=True)
def list_sessions() -> list[ChatSession]:
"""All persisted chat sessions, most recently updated first. A session
file that fails to parse is skipped rather than failing the whole list."""
if not CHATS_DIR.is_dir():
return []
sessions = []
for path in CHATS_DIR.glob("*.json"):
try:
data = json.loads(path.read_text(encoding="utf-8"))
sessions.append(
ChatSession(
id=data["id"],
title=data["title"],
created_at=data["created_at"],
updated_at=data["updated_at"],
history=[tuple(turn) for turn in data["history"]],
)
)
except (json.JSONDecodeError, OSError, KeyError, TypeError):
continue
sessions.sort(key=lambda s: s.updated_at, reverse=True)
return sessions
def build_messages(history: list[tuple[str, str]], question: str) -> list[dict[str, str]]:
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
for user_turn, assistant_turn in history[-MAX_HISTORY_TURNS:]: