feat: improve session navigation
This commit is contained in:
@@ -72,6 +72,24 @@ class ChatService:
|
||||
def list_sessions(self) -> list[ChatSession]:
|
||||
return self.store.list_sessions()
|
||||
|
||||
def search_sessions(self, text: str) -> list[ChatSession]:
|
||||
normalized = text.strip()
|
||||
if not normalized:
|
||||
raise ValueError("Für die Suche fehlt ein Text.")
|
||||
return self.store.search_sessions(normalized)
|
||||
|
||||
def resolve_session(self, reference: str) -> LoadedSession:
|
||||
session = self.store.resolve_unique_prefix(reference)
|
||||
return self.load_session(session.id)
|
||||
|
||||
def rename_session(self, session_id: str, title: str) -> ChatSession:
|
||||
normalized = " ".join(title.split()).strip()
|
||||
if not normalized:
|
||||
raise ValueError("Der Sitzungstitel darf nicht leer sein.")
|
||||
if len(normalized) > 60:
|
||||
normalized = f"{normalized[:59].rstrip()}…"
|
||||
return self.store.rename_session(session_id, normalized)
|
||||
|
||||
def load_session(self, session_id: str) -> LoadedSession:
|
||||
session = self.store.get_session(session_id)
|
||||
supports_session = getattr(self.provider, "supports_session", None)
|
||||
|
||||
@@ -14,6 +14,7 @@ from javis.config.settings import ConfigurationError, Settings
|
||||
from javis.core.chat_service import ChatService, SessionProviderMismatchError
|
||||
from javis.core.provider_router import HybridProvider
|
||||
from javis.memory.sqlite_store import (
|
||||
AmbiguousSessionReferenceError,
|
||||
ChatSession,
|
||||
SessionNotFoundError,
|
||||
SessionStoreError,
|
||||
@@ -34,7 +35,9 @@ StatusFunction = Callable[[], list[str]]
|
||||
HELP_TEXT = """Befehle:
|
||||
/new neue Sitzung beginnen
|
||||
/sessions gespeicherte Sitzungen auflisten
|
||||
/load <ID> frühere Sitzung laden und fortsetzen
|
||||
/load <NR|ID> Sitzung per Listennummer oder eindeutigem ID-Anfang laden
|
||||
/search <TEXT> Titel und eigene Nachrichten durchsuchen
|
||||
/rename <TITEL> aktive Sitzung umbenennen
|
||||
/clear Nachrichten der aktiven Sitzung leeren
|
||||
/provider aktiven Providermodus anzeigen
|
||||
/provider <MODUS> Modus auto, local oder gemini setzen
|
||||
@@ -139,10 +142,11 @@ def _show_sessions(sessions: list[ChatSession], output: OutputFunction) -> None:
|
||||
if not sessions:
|
||||
output("Keine gespeicherten Sitzungen vorhanden.")
|
||||
return
|
||||
for session in sessions:
|
||||
for number, session in enumerate(sessions, start=1):
|
||||
date = session.updated_at[:10]
|
||||
output(
|
||||
f"{session.id} | {session.updated_at} | "
|
||||
f"{session.provider}/{session.model} | {session.message_count} Nachrichten"
|
||||
f"{number}. {session.id[:8]} | {session.title} | {date} | "
|
||||
f"{session.message_count} Nachrichten | {session.last_provider}"
|
||||
)
|
||||
|
||||
|
||||
@@ -207,6 +211,7 @@ def run_chat(
|
||||
return 2
|
||||
|
||||
output("Lokaler Javis-Chat. /help zeigt die Befehle.")
|
||||
shown_sessions: list[ChatSession] = []
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -232,10 +237,33 @@ def run_chat(
|
||||
continue
|
||||
if entered == "/sessions":
|
||||
try:
|
||||
_show_sessions(service.list_sessions(), output)
|
||||
shown_sessions = service.list_sessions()
|
||||
_show_sessions(shown_sessions, output)
|
||||
except SessionStoreError as exc:
|
||||
output(f"Fehler: {exc}")
|
||||
continue
|
||||
if entered.startswith("/search"):
|
||||
parts = entered.split(maxsplit=1)
|
||||
if len(parts) != 2 or not parts[1].strip():
|
||||
output("Verwendung: /search <Text>")
|
||||
continue
|
||||
try:
|
||||
shown_sessions = service.search_sessions(parts[1])
|
||||
_show_sessions(shown_sessions, output)
|
||||
except (SessionStoreError, ValueError) as exc:
|
||||
output(f"Fehler: {exc}")
|
||||
continue
|
||||
if entered.startswith("/rename"):
|
||||
parts = entered.split(maxsplit=1)
|
||||
if len(parts) != 2 or not parts[1].strip():
|
||||
output("Verwendung: /rename <neuer Titel>")
|
||||
continue
|
||||
try:
|
||||
renamed = service.rename_session(active_id, parts[1])
|
||||
output(f"Sitzung umbenannt: {renamed.title}")
|
||||
except (SessionNotFoundError, SessionStoreError, ValueError) as exc:
|
||||
output(f"Fehler: {exc}")
|
||||
continue
|
||||
if entered == "/clear":
|
||||
try:
|
||||
service.clear_session(active_id)
|
||||
@@ -285,12 +313,24 @@ def run_chat(
|
||||
output("Verwendung: /load <Sitzungs-ID>")
|
||||
continue
|
||||
try:
|
||||
loaded = service.load_session(parts[1].strip())
|
||||
reference = parts[1].strip()
|
||||
if reference.isdecimal():
|
||||
number = int(reference)
|
||||
if not shown_sessions:
|
||||
output("Zuerst /sessions oder /search anzeigen.")
|
||||
continue
|
||||
if number < 1 or number > len(shown_sessions):
|
||||
output("Diese Listennummer ist nicht vorhanden.")
|
||||
continue
|
||||
loaded = service.load_session(shown_sessions[number - 1].id)
|
||||
else:
|
||||
loaded = service.resolve_session(reference)
|
||||
active_id = loaded.session.id
|
||||
output(f"Sitzung geladen: {active_id}")
|
||||
_show_history(service, active_id, output)
|
||||
except (
|
||||
SessionNotFoundError,
|
||||
AmbiguousSessionReferenceError,
|
||||
SessionProviderMismatchError,
|
||||
SessionStoreError,
|
||||
) as exc:
|
||||
|
||||
@@ -21,6 +21,10 @@ class SessionNotFoundError(SessionStoreError):
|
||||
"""A requested session ID does not exist."""
|
||||
|
||||
|
||||
class AmbiguousSessionReferenceError(SessionStoreError):
|
||||
"""A short session reference matches multiple sessions."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ChatSession:
|
||||
id: str
|
||||
@@ -188,6 +192,84 @@ class SQLiteSessionStore:
|
||||
raise SessionStoreError("Die Sitzungen konnten nicht aufgelistet werden.") from exc
|
||||
return [self._session_from_row(row) for row in rows]
|
||||
|
||||
def search_sessions(self, text: str, *, limit: int = 20) -> list[ChatSession]:
|
||||
normalized = text.strip()
|
||||
if not normalized:
|
||||
return []
|
||||
escaped = normalized.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
pattern = f"%{escaped}%"
|
||||
try:
|
||||
with self._connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT s.*, COUNT(m.id) AS message_count
|
||||
FROM sessions AS s
|
||||
LEFT JOIN messages AS m ON m.session_id = s.id
|
||||
WHERE s.title LIKE ? ESCAPE '\\' COLLATE NOCASE
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM messages AS searched
|
||||
WHERE searched.session_id = s.id
|
||||
AND searched.role = 'user'
|
||||
AND searched.content LIKE ? ESCAPE '\\' COLLATE NOCASE
|
||||
)
|
||||
GROUP BY s.id
|
||||
ORDER BY s.updated_at DESC, s.created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(pattern, pattern, max(1, min(limit, 100))),
|
||||
).fetchall()
|
||||
except sqlite3.Error as exc:
|
||||
raise SessionStoreError("Die Sitzungssuche ist fehlgeschlagen.") from exc
|
||||
return [self._session_from_row(row) for row in rows]
|
||||
|
||||
def resolve_unique_prefix(self, prefix: str) -> ChatSession:
|
||||
normalized = prefix.strip()
|
||||
if not normalized:
|
||||
raise SessionNotFoundError("Leerer Sitzungsverweis.")
|
||||
escaped = normalized.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
try:
|
||||
with self._connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM sessions
|
||||
WHERE id LIKE ? ESCAPE '\\'
|
||||
ORDER BY id
|
||||
LIMIT 2
|
||||
""",
|
||||
(f"{escaped}%",),
|
||||
).fetchall()
|
||||
except sqlite3.Error as exc:
|
||||
raise SessionStoreError("Der Sitzungsverweis konnte nicht geprüft werden.") from exc
|
||||
if not rows:
|
||||
raise SessionNotFoundError(f"Keine Sitzung beginnt mit '{normalized}'.")
|
||||
if len(rows) > 1:
|
||||
raise AmbiguousSessionReferenceError(
|
||||
f"Der Sitzungsanfang '{normalized}' ist nicht eindeutig."
|
||||
)
|
||||
return self.get_session(rows[0]["id"])
|
||||
|
||||
def rename_session(self, session_id: str, title: str) -> ChatSession:
|
||||
now = _utc_now()
|
||||
try:
|
||||
with self._connection() as connection:
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
UPDATE sessions
|
||||
SET title = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(title, now, session_id),
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
raise SessionNotFoundError(f"Sitzung '{session_id}' wurde nicht gefunden.")
|
||||
except SessionNotFoundError:
|
||||
raise
|
||||
except sqlite3.Error as exc:
|
||||
raise SessionStoreError("Die Sitzung konnte nicht umbenannt werden.") from exc
|
||||
return self.get_session(session_id)
|
||||
|
||||
def get_messages(self, session_id: str) -> list[ChatMessage]:
|
||||
self.get_session(session_id)
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user