appkit-assistant 0.17.3__py3-none-any.whl → 1.0.1__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.
Files changed (57) hide show
  1. appkit_assistant/backend/{models.py → database/models.py} +32 -132
  2. appkit_assistant/backend/{repositories.py → database/repositories.py} +93 -1
  3. appkit_assistant/backend/model_manager.py +5 -5
  4. appkit_assistant/backend/models/__init__.py +28 -0
  5. appkit_assistant/backend/models/anthropic.py +31 -0
  6. appkit_assistant/backend/models/google.py +27 -0
  7. appkit_assistant/backend/models/openai.py +50 -0
  8. appkit_assistant/backend/models/perplexity.py +56 -0
  9. appkit_assistant/backend/processors/__init__.py +29 -0
  10. appkit_assistant/backend/processors/claude_responses_processor.py +205 -387
  11. appkit_assistant/backend/processors/gemini_responses_processor.py +290 -352
  12. appkit_assistant/backend/processors/lorem_ipsum_processor.py +6 -4
  13. appkit_assistant/backend/processors/mcp_mixin.py +297 -0
  14. appkit_assistant/backend/processors/openai_base.py +11 -125
  15. appkit_assistant/backend/processors/openai_chat_completion_processor.py +5 -3
  16. appkit_assistant/backend/processors/openai_responses_processor.py +480 -402
  17. appkit_assistant/backend/processors/perplexity_processor.py +156 -79
  18. appkit_assistant/backend/{processor.py → processors/processor_base.py} +7 -2
  19. appkit_assistant/backend/processors/streaming_base.py +188 -0
  20. appkit_assistant/backend/schemas.py +138 -0
  21. appkit_assistant/backend/services/auth_error_detector.py +99 -0
  22. appkit_assistant/backend/services/chunk_factory.py +273 -0
  23. appkit_assistant/backend/services/citation_handler.py +292 -0
  24. appkit_assistant/backend/services/file_cleanup_service.py +316 -0
  25. appkit_assistant/backend/services/file_upload_service.py +903 -0
  26. appkit_assistant/backend/services/file_validation.py +138 -0
  27. appkit_assistant/backend/{mcp_auth_service.py → services/mcp_auth_service.py} +4 -2
  28. appkit_assistant/backend/services/mcp_token_service.py +61 -0
  29. appkit_assistant/backend/services/message_converter.py +289 -0
  30. appkit_assistant/backend/services/openai_client_service.py +120 -0
  31. appkit_assistant/backend/{response_accumulator.py → services/response_accumulator.py} +163 -1
  32. appkit_assistant/backend/services/system_prompt_builder.py +89 -0
  33. appkit_assistant/backend/services/thread_service.py +5 -3
  34. appkit_assistant/backend/system_prompt_cache.py +3 -3
  35. appkit_assistant/components/__init__.py +8 -4
  36. appkit_assistant/components/composer.py +59 -24
  37. appkit_assistant/components/file_manager.py +623 -0
  38. appkit_assistant/components/mcp_server_dialogs.py +12 -20
  39. appkit_assistant/components/mcp_server_table.py +12 -2
  40. appkit_assistant/components/message.py +119 -2
  41. appkit_assistant/components/thread.py +1 -1
  42. appkit_assistant/components/threadlist.py +4 -2
  43. appkit_assistant/components/tools_modal.py +37 -20
  44. appkit_assistant/configuration.py +12 -0
  45. appkit_assistant/state/file_manager_state.py +697 -0
  46. appkit_assistant/state/mcp_oauth_state.py +3 -3
  47. appkit_assistant/state/mcp_server_state.py +47 -2
  48. appkit_assistant/state/system_prompt_state.py +1 -1
  49. appkit_assistant/state/thread_list_state.py +99 -5
  50. appkit_assistant/state/thread_state.py +88 -9
  51. {appkit_assistant-0.17.3.dist-info → appkit_assistant-1.0.1.dist-info}/METADATA +8 -6
  52. appkit_assistant-1.0.1.dist-info/RECORD +58 -0
  53. appkit_assistant/backend/processors/claude_base.py +0 -178
  54. appkit_assistant/backend/processors/gemini_base.py +0 -84
  55. appkit_assistant-0.17.3.dist-info/RECORD +0 -39
  56. /appkit_assistant/backend/{file_manager.py → services/file_manager.py} +0 -0
  57. {appkit_assistant-0.17.3.dist-info → appkit_assistant-1.0.1.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)}