File size: 4,838 Bytes
5df8a73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""SessionManager adapter backed by DeepTutor's SQLite store.

Implements the SessionManager interface (get_or_create, save, list_sessions) but
reads/writes through DeepTutor's SQLiteSessionStore, unifying conversation history
for TutorBot and DeepTutor in a single database.
"""

from __future__ import annotations

import asyncio
from datetime import datetime
from pathlib import Path
from typing import Any

from loguru import logger

from deeptutor.tutorbot.session.manager import Session


class SQLiteSessionAdapter:
    """Drop-in replacement for SessionManager, backed by DeepTutor SQLite."""

    def __init__(self, store: Any) -> None:
        """
        Args:
            store: A DeepTutor SQLiteSessionStore instance.
        """
        self.store = store
        self._cache: dict[str, Session] = {}

    @property
    def sessions_dir(self) -> Path:
        """Compatibility stub — not used when persisting to SQLite."""
        return Path("/dev/null")

    @property
    def workspace(self) -> Path:
        return Path("/dev/null")

    def _session_id(self, key: str) -> str:
        """Derive a stable DeepTutor session_id from a TutorBot key (channel:chat_id)."""
        return f"tutorbot:{key}"

    def get_or_create(self, key: str) -> Session:
        """Get or create a session synchronously (loads from SQLite via event loop)."""
        if key in self._cache:
            return self._cache[key]

        session = self._load_sync(key)
        if session is None:
            session = Session(key=key)
            self._ensure_sqlite_session_sync(key)
        self._cache[key] = session
        return session

    def save(self, session: Session) -> None:
        """Persist session messages to SQLite synchronously."""
        try:
            loop = asyncio.get_running_loop()
            loop.create_task(self._save_async(session))
        except RuntimeError:
            asyncio.run(self._save_async(session))

    def invalidate(self, key: str) -> None:
        self._cache.pop(key, None)

    def list_sessions(self) -> list[dict[str, Any]]:
        try:
            loop = asyncio.get_running_loop()
            future = asyncio.ensure_future(self.store.list_sessions(limit=50))
            if loop.is_running():
                return []
            return loop.run_until_complete(future)
        except RuntimeError:
            return asyncio.run(self.store.list_sessions(limit=50))

    def _load_sync(self, key: str) -> Session | None:
        """Load a session from SQLite by running the coroutine."""
        session_id = self._session_id(key)
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = None

        if loop and loop.is_running():
            return None

        try:
            coro = self.store.get_messages_for_context(session_id)
            messages_raw = asyncio.run(coro) if not loop else loop.run_until_complete(coro)
        except Exception:
            return None

        if not messages_raw:
            return None

        messages = [
            {"role": m["role"], "content": m.get("content", ""), "timestamp": ""}
            for m in messages_raw
        ]
        return Session(key=key, messages=messages)

    def _ensure_sqlite_session_sync(self, key: str) -> None:
        """Ensure a corresponding DeepTutor session row exists."""
        session_id = self._session_id(key)
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = None

        coro = self.store.create_session(title=f"TutorBot: {key}", session_id=session_id)
        try:
            if loop and loop.is_running():
                loop.create_task(coro)
            elif loop:
                loop.run_until_complete(coro)
            else:
                asyncio.run(coro)
        except Exception:
            logger.debug("Session {} may already exist", session_id)

    async def _save_async(self, session: Session) -> None:
        """Write new messages to SQLite."""
        session_id = self._session_id(session.key)

        existing = await self.store.get_session(session_id)
        if existing is None:
            await self.store.create_session(
                title=f"TutorBot: {session.key}", session_id=session_id,
            )

        existing_msgs = await self.store.get_messages(session_id)
        existing_count = len(existing_msgs)

        for msg in session.messages[existing_count:]:
            role = msg.get("role", "user")
            content = msg.get("content", "")
            if role in ("user", "assistant") and content:
                await self.store.add_message(
                    session_id=session_id,
                    role=role,
                    content=content,
                    capability="tutorbot",
                )