appkit-assistant 0.9.0__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appkit_assistant/backend/models.py +50 -3
- appkit_assistant/backend/processors/openai_responses_processor.py +13 -9
- appkit_assistant/backend/repositories.py +141 -1
- appkit_assistant/components/__init__.py +2 -4
- appkit_assistant/components/mcp_server_dialogs.py +7 -2
- appkit_assistant/components/message.py +3 -3
- appkit_assistant/components/thread.py +8 -16
- appkit_assistant/components/threadlist.py +42 -29
- appkit_assistant/components/tools_modal.py +1 -1
- appkit_assistant/configuration.py +1 -0
- appkit_assistant/state/system_prompt_state.py +2 -4
- appkit_assistant/state/thread_list_state.py +271 -0
- appkit_assistant/state/thread_state.py +525 -608
- {appkit_assistant-0.9.0.dist-info → appkit_assistant-0.10.0.dist-info}/METADATA +2 -2
- {appkit_assistant-0.9.0.dist-info → appkit_assistant-0.10.0.dist-info}/RECORD +16 -15
- {appkit_assistant-0.9.0.dist-info → appkit_assistant-0.10.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Thread list state management for the assistant.
|
|
2
|
+
|
|
3
|
+
This module contains ThreadListState which manages the thread list sidebar:
|
|
4
|
+
- Loading thread summaries from database
|
|
5
|
+
- Adding new threads to the list (called by ThreadState)
|
|
6
|
+
- Deleting threads from database and list
|
|
7
|
+
- Tracking which thread is currently active/loading
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from collections.abc import AsyncGenerator
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
import reflex as rx
|
|
17
|
+
|
|
18
|
+
from appkit_assistant.backend.models import ThreadModel
|
|
19
|
+
from appkit_assistant.backend.repositories import ThreadRepository
|
|
20
|
+
from appkit_user.authentication.states import UserSession
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from appkit_assistant.state.thread_state import ThreadState
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ThreadListState(rx.State):
|
|
29
|
+
"""State for managing the thread list sidebar.
|
|
30
|
+
|
|
31
|
+
Responsibilities:
|
|
32
|
+
- Loading thread summaries from database on initialization
|
|
33
|
+
- Adding new threads to the list (called by ThreadState)
|
|
34
|
+
- Deleting threads from database and list
|
|
35
|
+
- Tracking active/loading thread IDs
|
|
36
|
+
|
|
37
|
+
Does NOT:
|
|
38
|
+
- Create new threads (ThreadState.new_thread does this)
|
|
39
|
+
- Load full thread data (ThreadState.get_thread does this)
|
|
40
|
+
- Persist thread data (ThreadState handles this)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Public state
|
|
44
|
+
threads: list[ThreadModel] = []
|
|
45
|
+
active_thread_id: str = ""
|
|
46
|
+
loading_thread_id: str = ""
|
|
47
|
+
loading: bool = True
|
|
48
|
+
|
|
49
|
+
# Private state
|
|
50
|
+
_initialized: bool = False
|
|
51
|
+
_current_user_id: str = ""
|
|
52
|
+
|
|
53
|
+
# -------------------------------------------------------------------------
|
|
54
|
+
# Computed properties
|
|
55
|
+
# -------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
@rx.var
|
|
58
|
+
def has_threads(self) -> bool:
|
|
59
|
+
"""Check if there are any threads."""
|
|
60
|
+
return len(self.threads) > 0
|
|
61
|
+
|
|
62
|
+
# -------------------------------------------------------------------------
|
|
63
|
+
# Initialization
|
|
64
|
+
# -------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
@rx.event(background=True)
|
|
67
|
+
async def initialize(self) -> AsyncGenerator[Any, Any]:
|
|
68
|
+
"""Initialize thread list - load summaries from database."""
|
|
69
|
+
async with self:
|
|
70
|
+
if self._initialized:
|
|
71
|
+
return
|
|
72
|
+
self.loading = True
|
|
73
|
+
yield
|
|
74
|
+
|
|
75
|
+
async for _ in self._load_threads():
|
|
76
|
+
yield
|
|
77
|
+
|
|
78
|
+
async def _load_threads(self) -> AsyncGenerator[Any, Any]:
|
|
79
|
+
"""Load thread summaries from database (internal)."""
|
|
80
|
+
# Late import to avoid circular dependency
|
|
81
|
+
from appkit_assistant.state.thread_state import ThreadState # noqa: PLC0415
|
|
82
|
+
|
|
83
|
+
async with self:
|
|
84
|
+
user_session: UserSession = await self.get_state(UserSession)
|
|
85
|
+
current_user_id = user_session.user.user_id if user_session.user else ""
|
|
86
|
+
is_authenticated = await user_session.is_authenticated
|
|
87
|
+
|
|
88
|
+
# Handle user change
|
|
89
|
+
if self._current_user_id != current_user_id:
|
|
90
|
+
logger.info(
|
|
91
|
+
"User changed from '%s' to '%s' - resetting state",
|
|
92
|
+
self._current_user_id or "(none)",
|
|
93
|
+
current_user_id or "(none)",
|
|
94
|
+
)
|
|
95
|
+
self._initialized = False
|
|
96
|
+
self._current_user_id = current_user_id
|
|
97
|
+
self._clear_threads()
|
|
98
|
+
yield
|
|
99
|
+
|
|
100
|
+
# Reset ThreadState
|
|
101
|
+
thread_state: ThreadState = await self.get_state(ThreadState)
|
|
102
|
+
thread_state.new_thread()
|
|
103
|
+
|
|
104
|
+
if self._initialized:
|
|
105
|
+
self.loading = False
|
|
106
|
+
yield
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Check authentication
|
|
110
|
+
if not is_authenticated:
|
|
111
|
+
self._clear_threads()
|
|
112
|
+
self._current_user_id = ""
|
|
113
|
+
self.loading = False
|
|
114
|
+
yield
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
user_id = user_session.user.user_id if user_session.user else None
|
|
118
|
+
|
|
119
|
+
if not user_id:
|
|
120
|
+
async with self:
|
|
121
|
+
self.loading = False
|
|
122
|
+
yield
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Fetch threads from database
|
|
126
|
+
try:
|
|
127
|
+
threads = await ThreadRepository.get_summaries_by_user(user_id)
|
|
128
|
+
async with self:
|
|
129
|
+
self.threads = threads
|
|
130
|
+
self._initialized = True
|
|
131
|
+
logger.debug("Loaded %d threads", len(threads))
|
|
132
|
+
yield
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error("Error loading threads: %s", e)
|
|
135
|
+
async with self:
|
|
136
|
+
self._clear_threads()
|
|
137
|
+
yield
|
|
138
|
+
finally:
|
|
139
|
+
async with self:
|
|
140
|
+
self.loading = False
|
|
141
|
+
yield
|
|
142
|
+
|
|
143
|
+
# -------------------------------------------------------------------------
|
|
144
|
+
# Thread list management
|
|
145
|
+
# -------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
async def add_thread(self, thread: ThreadModel) -> None:
|
|
148
|
+
"""Add a new thread to the list.
|
|
149
|
+
|
|
150
|
+
Called by ThreadState via get_state() after first successful response.
|
|
151
|
+
Not an @rx.event so it can be called directly from background tasks.
|
|
152
|
+
Does not persist to DB - ThreadState handles persistence.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
thread: The thread model to add.
|
|
156
|
+
"""
|
|
157
|
+
# Check if already in list (idempotent)
|
|
158
|
+
existing = next(
|
|
159
|
+
(t for t in self.threads if t.thread_id == thread.thread_id),
|
|
160
|
+
None,
|
|
161
|
+
)
|
|
162
|
+
if existing:
|
|
163
|
+
logger.debug("Thread already in list: %s", thread.thread_id)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Deactivate other threads
|
|
167
|
+
self.threads = [
|
|
168
|
+
ThreadModel(**{**t.model_dump(), "active": False}) for t in self.threads
|
|
169
|
+
]
|
|
170
|
+
# Add new thread at beginning (mark as active)
|
|
171
|
+
thread.active = True
|
|
172
|
+
self.threads = [thread, *self.threads]
|
|
173
|
+
self.active_thread_id = thread.thread_id
|
|
174
|
+
logger.debug("Added thread to list: %s", thread.thread_id)
|
|
175
|
+
|
|
176
|
+
@rx.event(background=True)
|
|
177
|
+
async def delete_thread(self, thread_id: str) -> AsyncGenerator[Any, Any]:
|
|
178
|
+
"""Delete a thread from database and list.
|
|
179
|
+
|
|
180
|
+
If the deleted thread was the active thread, resets ThreadState
|
|
181
|
+
to show an empty thread.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
thread_id: The ID of the thread to delete.
|
|
185
|
+
"""
|
|
186
|
+
# Late import to avoid circular dependency
|
|
187
|
+
from appkit_assistant.state.thread_state import ThreadState # noqa: PLC0415
|
|
188
|
+
|
|
189
|
+
async with self:
|
|
190
|
+
user_session: UserSession = await self.get_state(UserSession)
|
|
191
|
+
is_authenticated = await user_session.is_authenticated
|
|
192
|
+
user_id = user_session.user.user_id if user_session.user else None
|
|
193
|
+
|
|
194
|
+
thread_to_delete = next(
|
|
195
|
+
(t for t in self.threads if t.thread_id == thread_id), None
|
|
196
|
+
)
|
|
197
|
+
was_active = thread_id == self.active_thread_id
|
|
198
|
+
|
|
199
|
+
if not is_authenticated or not user_id:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if not thread_to_delete:
|
|
203
|
+
yield rx.toast.error(
|
|
204
|
+
"Chat nicht gefunden.", position="top-right", close_button=True
|
|
205
|
+
)
|
|
206
|
+
logger.warning("Thread %s not found for deletion", thread_id)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Delete from database
|
|
211
|
+
await ThreadRepository.delete_thread(thread_id, user_id)
|
|
212
|
+
|
|
213
|
+
async with self:
|
|
214
|
+
# Remove from list
|
|
215
|
+
self.threads = [t for t in self.threads if t.thread_id != thread_id]
|
|
216
|
+
|
|
217
|
+
if was_active:
|
|
218
|
+
self.active_thread_id = ""
|
|
219
|
+
# Reset ThreadState to empty thread
|
|
220
|
+
thread_state: ThreadState = await self.get_state(ThreadState)
|
|
221
|
+
thread_state.new_thread()
|
|
222
|
+
|
|
223
|
+
yield rx.toast.info(
|
|
224
|
+
f"Chat '{thread_to_delete.title}' gelöscht.",
|
|
225
|
+
position="top-right",
|
|
226
|
+
close_button=True,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error("Error deleting thread %s: %s", thread_id, e)
|
|
231
|
+
yield rx.toast.error(
|
|
232
|
+
"Fehler beim Löschen des Chats.",
|
|
233
|
+
position="top-right",
|
|
234
|
+
close_button=True,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# -------------------------------------------------------------------------
|
|
238
|
+
# Logout handling
|
|
239
|
+
# -------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
@rx.event
|
|
242
|
+
async def reset_on_logout(self) -> None:
|
|
243
|
+
"""Reset state on user logout to prevent data leakage."""
|
|
244
|
+
# Late import to avoid circular dependency
|
|
245
|
+
from appkit_assistant.state.thread_state import ThreadState # noqa: PLC0415
|
|
246
|
+
|
|
247
|
+
logger.info(
|
|
248
|
+
"Resetting ThreadListState on logout for user: %s",
|
|
249
|
+
self._current_user_id,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
self._clear_threads()
|
|
253
|
+
self.loading = False
|
|
254
|
+
self._initialized = False
|
|
255
|
+
self._current_user_id = ""
|
|
256
|
+
|
|
257
|
+
# Reset ThreadState
|
|
258
|
+
thread_state: ThreadState = await self.get_state(ThreadState)
|
|
259
|
+
thread_state.new_thread()
|
|
260
|
+
|
|
261
|
+
logger.debug("ThreadListState reset complete")
|
|
262
|
+
|
|
263
|
+
# -------------------------------------------------------------------------
|
|
264
|
+
# Internal helpers
|
|
265
|
+
# -------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def _clear_threads(self) -> None:
|
|
268
|
+
"""Clear thread-related state."""
|
|
269
|
+
self.threads = []
|
|
270
|
+
self.active_thread_id = ""
|
|
271
|
+
self.loading_thread_id = ""
|