appkit-assistant 0.17.3__py3-none-any.whl → 1.0.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 → database/models.py} +32 -132
- appkit_assistant/backend/{repositories.py → database/repositories.py} +93 -1
- appkit_assistant/backend/model_manager.py +5 -5
- appkit_assistant/backend/models/__init__.py +28 -0
- appkit_assistant/backend/models/anthropic.py +31 -0
- appkit_assistant/backend/models/google.py +27 -0
- appkit_assistant/backend/models/openai.py +50 -0
- appkit_assistant/backend/models/perplexity.py +56 -0
- appkit_assistant/backend/processors/__init__.py +29 -0
- appkit_assistant/backend/processors/claude_responses_processor.py +205 -387
- appkit_assistant/backend/processors/gemini_responses_processor.py +231 -299
- appkit_assistant/backend/processors/lorem_ipsum_processor.py +6 -4
- appkit_assistant/backend/processors/mcp_mixin.py +297 -0
- appkit_assistant/backend/processors/openai_base.py +11 -125
- appkit_assistant/backend/processors/openai_chat_completion_processor.py +5 -3
- appkit_assistant/backend/processors/openai_responses_processor.py +480 -402
- appkit_assistant/backend/processors/perplexity_processor.py +156 -79
- appkit_assistant/backend/{processor.py → processors/processor_base.py} +7 -2
- appkit_assistant/backend/processors/streaming_base.py +188 -0
- appkit_assistant/backend/schemas.py +138 -0
- appkit_assistant/backend/services/auth_error_detector.py +99 -0
- appkit_assistant/backend/services/chunk_factory.py +273 -0
- appkit_assistant/backend/services/citation_handler.py +292 -0
- appkit_assistant/backend/services/file_cleanup_service.py +316 -0
- appkit_assistant/backend/services/file_upload_service.py +903 -0
- appkit_assistant/backend/services/file_validation.py +138 -0
- appkit_assistant/backend/{mcp_auth_service.py → services/mcp_auth_service.py} +4 -2
- appkit_assistant/backend/services/mcp_token_service.py +61 -0
- appkit_assistant/backend/services/message_converter.py +289 -0
- appkit_assistant/backend/services/openai_client_service.py +120 -0
- appkit_assistant/backend/{response_accumulator.py → services/response_accumulator.py} +163 -1
- appkit_assistant/backend/services/system_prompt_builder.py +89 -0
- appkit_assistant/backend/services/thread_service.py +5 -3
- appkit_assistant/backend/system_prompt_cache.py +3 -3
- appkit_assistant/components/__init__.py +8 -4
- appkit_assistant/components/composer.py +59 -24
- appkit_assistant/components/file_manager.py +623 -0
- appkit_assistant/components/mcp_server_dialogs.py +12 -20
- appkit_assistant/components/mcp_server_table.py +12 -2
- appkit_assistant/components/message.py +119 -2
- appkit_assistant/components/thread.py +1 -1
- appkit_assistant/components/threadlist.py +4 -2
- appkit_assistant/components/tools_modal.py +37 -20
- appkit_assistant/configuration.py +12 -0
- appkit_assistant/state/file_manager_state.py +697 -0
- appkit_assistant/state/mcp_oauth_state.py +3 -3
- appkit_assistant/state/mcp_server_state.py +47 -2
- appkit_assistant/state/system_prompt_state.py +1 -1
- appkit_assistant/state/thread_list_state.py +99 -5
- appkit_assistant/state/thread_state.py +88 -9
- {appkit_assistant-0.17.3.dist-info → appkit_assistant-1.0.0.dist-info}/METADATA +8 -6
- appkit_assistant-1.0.0.dist-info/RECORD +58 -0
- appkit_assistant/backend/processors/claude_base.py +0 -178
- appkit_assistant/backend/processors/gemini_base.py +0 -84
- appkit_assistant-0.17.3.dist-info/RECORD +0 -39
- /appkit_assistant/backend/{file_manager.py → services/file_manager.py} +0 -0
- {appkit_assistant-0.17.3.dist-info → appkit_assistant-1.0.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""File cleanup scheduler with APScheduler integration.
|
|
2
|
+
|
|
3
|
+
Provides scheduled cleanup of expired OpenAI vector stores and associated files.
|
|
4
|
+
The scheduler runs as part of the FastAPI app lifecycle.
|
|
5
|
+
|
|
6
|
+
Note: HTTP endpoints have been removed for security. Use `run_cleanup()` for
|
|
7
|
+
manual triggers from Reflex UI or internal code.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import AsyncGenerator
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
15
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
16
|
+
from openai import AsyncOpenAI, NotFoundError
|
|
17
|
+
from sqlalchemy import select
|
|
18
|
+
|
|
19
|
+
from appkit_assistant.backend.database.models import (
|
|
20
|
+
AssistantFileUpload,
|
|
21
|
+
AssistantThread,
|
|
22
|
+
)
|
|
23
|
+
from appkit_assistant.backend.services.file_upload_service import FileUploadService
|
|
24
|
+
from appkit_assistant.backend.services.openai_client_service import (
|
|
25
|
+
OpenAIClientService,
|
|
26
|
+
)
|
|
27
|
+
from appkit_assistant.configuration import AssistantConfig, FileUploadConfig
|
|
28
|
+
from appkit_commons.database.session import get_asyncdb_session
|
|
29
|
+
from appkit_commons.registry import service_registry
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Global scheduler instance
|
|
34
|
+
_scheduler: AsyncIOScheduler | None = None
|
|
35
|
+
_cleanup_service: "FileCleanupService | None" = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FileCleanupService:
|
|
39
|
+
"""Service for cleaning up expired files and vector stores.
|
|
40
|
+
|
|
41
|
+
Delegates actual cleanup operations to FileUploadService.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
client: AsyncOpenAI,
|
|
47
|
+
file_upload_service: FileUploadService,
|
|
48
|
+
config: FileUploadConfig,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize the cleanup service.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
client: AsyncOpenAI client for checking vector store status.
|
|
54
|
+
file_upload_service: Service for file/vector store operations.
|
|
55
|
+
config: File upload configuration.
|
|
56
|
+
"""
|
|
57
|
+
self._client = client
|
|
58
|
+
self._file_upload_service = file_upload_service
|
|
59
|
+
self.config = config
|
|
60
|
+
|
|
61
|
+
async def cleanup_expired_files(
|
|
62
|
+
self,
|
|
63
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
64
|
+
"""Clean up expired vector stores and their associated files.
|
|
65
|
+
|
|
66
|
+
This method:
|
|
67
|
+
1. Gets all unique vector store IDs from the database
|
|
68
|
+
2. Checks if each vector store still exists in OpenAI
|
|
69
|
+
3. For expired/deleted stores: delegates to FileUploadService
|
|
70
|
+
4. Clears vector_store_id from threads with expired stores
|
|
71
|
+
|
|
72
|
+
Yields:
|
|
73
|
+
Progress updates with current statistics and status.
|
|
74
|
+
"""
|
|
75
|
+
stats = {
|
|
76
|
+
"vector_stores_checked": 0,
|
|
77
|
+
"vector_stores_expired": 0,
|
|
78
|
+
"vector_stores_deleted": 0,
|
|
79
|
+
"threads_updated": 0,
|
|
80
|
+
"current_vector_store": None,
|
|
81
|
+
"total_vector_stores": 0,
|
|
82
|
+
"status": "starting",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Get all unique vector store IDs from file uploads
|
|
87
|
+
async with get_asyncdb_session() as session:
|
|
88
|
+
result = await session.execute(
|
|
89
|
+
select(AssistantFileUpload.vector_store_id).distinct()
|
|
90
|
+
)
|
|
91
|
+
vector_store_ids = [row[0] for row in result.all() if row[0]]
|
|
92
|
+
|
|
93
|
+
stats["total_vector_stores"] = len(vector_store_ids)
|
|
94
|
+
stats["status"] = "checking"
|
|
95
|
+
logger.info(
|
|
96
|
+
"Checking %d vector stores for expiration",
|
|
97
|
+
len(vector_store_ids),
|
|
98
|
+
)
|
|
99
|
+
yield stats.copy()
|
|
100
|
+
|
|
101
|
+
for vs_id in vector_store_ids:
|
|
102
|
+
stats["current_vector_store"] = vs_id
|
|
103
|
+
stats["vector_stores_checked"] += 1
|
|
104
|
+
yield stats.copy()
|
|
105
|
+
|
|
106
|
+
is_expired = await self._check_vector_store_expired(vs_id)
|
|
107
|
+
|
|
108
|
+
if is_expired:
|
|
109
|
+
stats["vector_stores_expired"] += 1
|
|
110
|
+
stats["status"] = "deleting"
|
|
111
|
+
yield stats.copy()
|
|
112
|
+
|
|
113
|
+
# Delegate cleanup to FileUploadService
|
|
114
|
+
deleted = await self._file_upload_service.delete_vector_store(vs_id)
|
|
115
|
+
if deleted:
|
|
116
|
+
stats["vector_stores_deleted"] += 1
|
|
117
|
+
# Clear vector_store_id from associated threads
|
|
118
|
+
threads_updated = await self._clear_thread_vector_store_ids(vs_id)
|
|
119
|
+
stats["threads_updated"] += threads_updated
|
|
120
|
+
stats["status"] = "checking"
|
|
121
|
+
yield stats.copy()
|
|
122
|
+
|
|
123
|
+
stats["status"] = "completed"
|
|
124
|
+
stats["current_vector_store"] = None
|
|
125
|
+
logger.info("Cleanup completed: %s", stats)
|
|
126
|
+
yield stats.copy()
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
stats["status"] = "error"
|
|
130
|
+
stats["error"] = str(e)
|
|
131
|
+
logger.error("Error during file cleanup: %s", e)
|
|
132
|
+
yield stats.copy()
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
async def _check_vector_store_expired(self, vector_store_id: str) -> bool:
|
|
136
|
+
"""Check if a vector store has expired or been deleted.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
vector_store_id: The OpenAI vector store ID to check.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if the vector store is expired/deleted, False otherwise.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
await self._client.vector_stores.retrieve(vector_store_id=vector_store_id)
|
|
146
|
+
except NotFoundError:
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
async def _clear_thread_vector_store_ids(self, vector_store_id: str) -> int:
|
|
152
|
+
"""Clear vector_store_id from all threads associated with the store.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
vector_store_id: The vector store ID to clear from threads.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Number of threads updated.
|
|
159
|
+
"""
|
|
160
|
+
updated_count = 0
|
|
161
|
+
async with get_asyncdb_session() as session:
|
|
162
|
+
thread_result = await session.execute(
|
|
163
|
+
select(AssistantThread).where(
|
|
164
|
+
AssistantThread.vector_store_id == vector_store_id
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
threads = list(thread_result.scalars().all())
|
|
168
|
+
|
|
169
|
+
for thread in threads:
|
|
170
|
+
thread.vector_store_id = None
|
|
171
|
+
session.add(thread)
|
|
172
|
+
updated_count += 1
|
|
173
|
+
logger.debug(
|
|
174
|
+
"Cleared vector_store_id from thread %s",
|
|
175
|
+
thread.thread_id,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
await session.commit()
|
|
179
|
+
|
|
180
|
+
return updated_count
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _get_file_upload_config() -> FileUploadConfig:
|
|
184
|
+
"""Get file upload configuration."""
|
|
185
|
+
try:
|
|
186
|
+
config: AssistantConfig | None = service_registry().get(AssistantConfig)
|
|
187
|
+
if config:
|
|
188
|
+
return config.file_upload
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning("Failed to get file upload config: %s", e)
|
|
191
|
+
return FileUploadConfig()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def _run_cleanup_job() -> None:
|
|
195
|
+
"""Scheduled job to run file cleanup."""
|
|
196
|
+
if not _cleanup_service:
|
|
197
|
+
logger.warning("Cleanup service not initialized, skipping cleanup job")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
logger.info("Starting scheduled file cleanup")
|
|
202
|
+
final_stats: dict[str, Any] = {}
|
|
203
|
+
async for progress in _cleanup_service.cleanup_expired_files():
|
|
204
|
+
final_stats = progress # Keep updating to get final stats
|
|
205
|
+
logger.info("Scheduled cleanup completed: %s", final_stats)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error("Scheduled cleanup failed: %s", e)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def start_scheduler() -> None:
|
|
211
|
+
"""Start the APScheduler for file cleanup."""
|
|
212
|
+
global _scheduler, _cleanup_service # noqa: PLW0603
|
|
213
|
+
|
|
214
|
+
# Get configuration
|
|
215
|
+
config = _get_file_upload_config()
|
|
216
|
+
|
|
217
|
+
# Create OpenAI client via service
|
|
218
|
+
client_service = OpenAIClientService.from_config()
|
|
219
|
+
if not client_service.is_available:
|
|
220
|
+
logger.warning("OpenAI client not available, file cleanup scheduler disabled")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
client = client_service.create_client()
|
|
224
|
+
if not client:
|
|
225
|
+
logger.warning("Failed to create OpenAI client, cleanup scheduler disabled")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Create FileUploadService and FileCleanupService
|
|
229
|
+
file_upload_service = FileUploadService(client=client, config=config)
|
|
230
|
+
_cleanup_service = FileCleanupService(
|
|
231
|
+
client=client, file_upload_service=file_upload_service, config=config
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Create scheduler
|
|
235
|
+
_scheduler = AsyncIOScheduler()
|
|
236
|
+
|
|
237
|
+
# Add cleanup job
|
|
238
|
+
_scheduler.add_job(
|
|
239
|
+
_run_cleanup_job,
|
|
240
|
+
trigger=IntervalTrigger(minutes=config.cleanup_interval_minutes),
|
|
241
|
+
id="file_cleanup",
|
|
242
|
+
name="Clean up expired OpenAI files and vector stores",
|
|
243
|
+
replace_existing=True,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
_scheduler.start()
|
|
247
|
+
logger.info(
|
|
248
|
+
"File cleanup scheduler started (interval: %d minutes)",
|
|
249
|
+
config.cleanup_interval_minutes,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def stop_scheduler() -> None:
|
|
254
|
+
"""Stop the APScheduler."""
|
|
255
|
+
global _scheduler # noqa: PLW0603
|
|
256
|
+
|
|
257
|
+
if _scheduler:
|
|
258
|
+
_scheduler.shutdown(wait=False)
|
|
259
|
+
_scheduler = None
|
|
260
|
+
logger.info("File cleanup scheduler stopped")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def run_cleanup() -> AsyncGenerator[dict[str, Any], None]:
|
|
264
|
+
"""Manually trigger file cleanup.
|
|
265
|
+
|
|
266
|
+
This async generator can be called from Reflex UI (e.g., filemanager) to
|
|
267
|
+
trigger the cleanup process and receive real-time progress updates.
|
|
268
|
+
|
|
269
|
+
Yields:
|
|
270
|
+
Dictionary with cleanup progress including:
|
|
271
|
+
- status: 'starting', 'checking', 'deleting', 'completed', or 'error'
|
|
272
|
+
- vector_stores_checked: number checked so far
|
|
273
|
+
- vector_stores_expired: number found expired
|
|
274
|
+
- vector_stores_deleted: number successfully deleted
|
|
275
|
+
- threads_updated: number of threads cleared
|
|
276
|
+
- current_vector_store: ID being processed (or None)
|
|
277
|
+
- total_vector_stores: total count to process
|
|
278
|
+
- error: error message (only if status is 'error')
|
|
279
|
+
"""
|
|
280
|
+
global _cleanup_service # noqa: PLW0603
|
|
281
|
+
|
|
282
|
+
if not _cleanup_service:
|
|
283
|
+
# Try to initialize on-demand
|
|
284
|
+
config = _get_file_upload_config()
|
|
285
|
+
client_service = OpenAIClientService.from_config()
|
|
286
|
+
|
|
287
|
+
if not client_service.is_available:
|
|
288
|
+
logger.warning("OpenAI client not available for manual cleanup")
|
|
289
|
+
yield {
|
|
290
|
+
"status": "error",
|
|
291
|
+
"error": "OpenAI client not available",
|
|
292
|
+
}
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
client = client_service.create_client()
|
|
296
|
+
if not client:
|
|
297
|
+
logger.warning("Failed to create OpenAI client for manual cleanup")
|
|
298
|
+
yield {
|
|
299
|
+
"status": "error",
|
|
300
|
+
"error": "Failed to create OpenAI client",
|
|
301
|
+
}
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
file_upload_service = FileUploadService(client=client, config=config)
|
|
305
|
+
_cleanup_service = FileCleanupService(
|
|
306
|
+
client=client, file_upload_service=file_upload_service, config=config
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
logger.info("Starting manual file cleanup")
|
|
311
|
+
async for stats in _cleanup_service.cleanup_expired_files():
|
|
312
|
+
yield stats
|
|
313
|
+
logger.info("Manual cleanup completed")
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error("Manual cleanup failed: %s", e)
|
|
316
|
+
yield {"status": "error", "error": str(e)}
|