django-cfg 1.1.81__py3-none-any.whl → 1.2.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.
Files changed (246) hide show
  1. django_cfg/__init__.py +20 -448
  2. django_cfg/apps/accounts/README.md +3 -3
  3. django_cfg/apps/accounts/admin/__init__.py +0 -2
  4. django_cfg/apps/accounts/admin/activity.py +2 -9
  5. django_cfg/apps/accounts/admin/filters.py +0 -42
  6. django_cfg/apps/accounts/admin/inlines.py +8 -8
  7. django_cfg/apps/accounts/admin/otp.py +5 -5
  8. django_cfg/apps/accounts/admin/registration_source.py +1 -8
  9. django_cfg/apps/accounts/admin/user.py +12 -20
  10. django_cfg/apps/accounts/managers/user_manager.py +2 -129
  11. django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
  12. django_cfg/apps/accounts/models.py +3 -123
  13. django_cfg/apps/accounts/serializers/otp.py +40 -44
  14. django_cfg/apps/accounts/serializers/profile.py +0 -2
  15. django_cfg/apps/accounts/services/otp_service.py +98 -186
  16. django_cfg/apps/accounts/signals.py +25 -15
  17. django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
  18. django_cfg/apps/accounts/views/otp.py +35 -36
  19. django_cfg/apps/agents/README.md +129 -0
  20. django_cfg/apps/agents/__init__.py +68 -0
  21. django_cfg/apps/agents/admin/__init__.py +17 -0
  22. django_cfg/apps/agents/admin/execution_admin.py +460 -0
  23. django_cfg/apps/agents/admin/registry_admin.py +360 -0
  24. django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
  25. django_cfg/apps/agents/apps.py +29 -0
  26. django_cfg/apps/agents/core/__init__.py +20 -0
  27. django_cfg/apps/agents/core/agent.py +281 -0
  28. django_cfg/apps/agents/core/dependencies.py +154 -0
  29. django_cfg/apps/agents/core/exceptions.py +66 -0
  30. django_cfg/apps/agents/core/models.py +106 -0
  31. django_cfg/apps/agents/core/orchestrator.py +391 -0
  32. django_cfg/apps/agents/examples/__init__.py +3 -0
  33. django_cfg/apps/agents/examples/simple_example.py +161 -0
  34. django_cfg/apps/agents/integration/__init__.py +14 -0
  35. django_cfg/apps/agents/integration/middleware.py +80 -0
  36. django_cfg/apps/agents/integration/registry.py +345 -0
  37. django_cfg/apps/agents/integration/signals.py +50 -0
  38. django_cfg/apps/agents/management/__init__.py +3 -0
  39. django_cfg/apps/agents/management/commands/__init__.py +3 -0
  40. django_cfg/apps/agents/management/commands/create_agent.py +365 -0
  41. django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
  42. django_cfg/apps/agents/managers/__init__.py +23 -0
  43. django_cfg/apps/agents/managers/execution.py +236 -0
  44. django_cfg/apps/agents/managers/registry.py +254 -0
  45. django_cfg/apps/agents/managers/toolsets.py +496 -0
  46. django_cfg/apps/agents/migrations/0001_initial.py +286 -0
  47. django_cfg/apps/agents/migrations/__init__.py +5 -0
  48. django_cfg/apps/agents/models/__init__.py +15 -0
  49. django_cfg/apps/agents/models/execution.py +215 -0
  50. django_cfg/apps/agents/models/registry.py +220 -0
  51. django_cfg/apps/agents/models/toolsets.py +305 -0
  52. django_cfg/apps/agents/patterns/__init__.py +24 -0
  53. django_cfg/apps/agents/patterns/content_agents.py +234 -0
  54. django_cfg/apps/agents/toolsets/__init__.py +15 -0
  55. django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
  56. django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
  57. django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
  58. django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
  59. django_cfg/apps/agents/urls.py +46 -0
  60. django_cfg/apps/knowbase/README.md +150 -0
  61. django_cfg/apps/knowbase/__init__.py +27 -0
  62. django_cfg/apps/knowbase/admin/__init__.py +23 -0
  63. django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
  64. django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
  65. django_cfg/apps/knowbase/admin/document_admin.py +650 -0
  66. django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
  67. django_cfg/apps/knowbase/apps.py +81 -0
  68. django_cfg/apps/knowbase/config/README.md +176 -0
  69. django_cfg/apps/knowbase/config/__init__.py +51 -0
  70. django_cfg/apps/knowbase/config/constance_fields.py +186 -0
  71. django_cfg/apps/knowbase/config/constance_settings.py +200 -0
  72. django_cfg/apps/knowbase/config/settings.py +444 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +79 -17
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/templates/guide.md +266 -0
  198. django_cfg/modules/django_twilio/twilio_service.py +2 -2
  199. django_cfg/modules/django_unfold/__init__.py +69 -0
  200. django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
  201. django_cfg/modules/django_unfold/dashboard.py +278 -0
  202. django_cfg/modules/django_unfold/icons/README.md +145 -0
  203. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  204. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  205. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  206. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  207. django_cfg/modules/django_unfold/models/config.py +601 -0
  208. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  209. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  210. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  211. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  212. django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
  213. django_cfg/modules/django_unfold/utils.py +140 -0
  214. django_cfg/registry/__init__.py +23 -0
  215. django_cfg/registry/core.py +61 -0
  216. django_cfg/registry/exceptions.py +11 -0
  217. django_cfg/registry/modules.py +12 -0
  218. django_cfg/registry/services.py +26 -0
  219. django_cfg/registry/third_party.py +52 -0
  220. django_cfg/routing/__init__.py +19 -0
  221. django_cfg/routing/callbacks.py +198 -0
  222. django_cfg/routing/routers.py +48 -0
  223. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  224. django_cfg/templatetags/__init__.py +0 -0
  225. django_cfg/templatetags/django_cfg.py +33 -0
  226. django_cfg/urls.py +33 -0
  227. django_cfg/utils/path_resolution.py +1 -1
  228. django_cfg/utils/smart_defaults.py +7 -61
  229. django_cfg/utils/toolkit.py +663 -0
  230. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
  231. django_cfg-1.2.0.dist-info/RECORD +441 -0
  232. django_cfg/apps/tasks/@docs/README.md +0 -195
  233. django_cfg/archive/django_sample.zip +0 -0
  234. django_cfg/models/unfold.py +0 -271
  235. django_cfg/modules/unfold/__init__.py +0 -29
  236. django_cfg/modules/unfold/dashboard.py +0 -318
  237. django_cfg/pyproject.toml +0 -370
  238. django_cfg/routers.py +0 -83
  239. django_cfg-1.1.81.dist-info/RECORD +0 -278
  240. /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
  241. /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
  242. /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
  243. /django_cfg/{version_check.py → utils/version_check.py} +0 -0
  244. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,341 @@
1
+ """
2
+ External Data processing tasks with Dramatiq.
3
+ """
4
+
5
+ import dramatiq
6
+ import logging
7
+ import time
8
+ from typing import Dict, List, Any, Tuple, Optional
9
+ from django.db import transaction
10
+ from django.utils import timezone
11
+
12
+ from ..models.external_data import ExternalData, ExternalDataChunk, ExternalDataStatus
13
+ from ..mixins.service import ExternalDataService
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dramatiq.actor(
19
+ queue_name="knowledge",
20
+ max_retries=3,
21
+ min_backoff=1000, # 1 second
22
+ max_backoff=30000, # 30 seconds
23
+ priority=5
24
+ )
25
+ def process_external_data_async(
26
+ external_data_id: str,
27
+ force_reprocess: bool = False
28
+ ) -> Dict[str, Any]:
29
+ """
30
+ Process external data asynchronously with full pipeline.
31
+
32
+ Args:
33
+ external_data_id: ExternalData UUID to process
34
+ force_reprocess: Force reprocessing even if already completed
35
+
36
+ Returns:
37
+ Processing results with statistics
38
+ """
39
+ start_time = time.time()
40
+
41
+ try:
42
+ with transaction.atomic():
43
+ # Get external data instance
44
+ external_data = ExternalData.objects.select_for_update().get(id=external_data_id)
45
+
46
+ # Check if already processing
47
+ if external_data.status == ExternalDataStatus.PROCESSING:
48
+ logger.warning(f"External data {external_data_id} is already being processed")
49
+ return {
50
+ 'success': False,
51
+ 'error': 'Already processing',
52
+ 'external_data_id': external_data_id
53
+ }
54
+
55
+ # Check if already completed and not forcing reprocess
56
+ if external_data.status == ExternalDataStatus.COMPLETED and not force_reprocess:
57
+ logger.info(f"External data {external_data_id} already completed, skipping")
58
+ return {
59
+ 'success': True,
60
+ 'skipped': True,
61
+ 'external_data_id': external_data_id,
62
+ 'total_chunks': external_data.total_chunks,
63
+ 'total_tokens': external_data.total_tokens,
64
+ 'processing_cost': external_data.processing_cost
65
+ }
66
+
67
+ # Check if has content
68
+ if not external_data.content or not external_data.content.strip():
69
+ logger.warning(f"External data {external_data_id} has no content to process")
70
+ external_data.status = ExternalDataStatus.COMPLETED
71
+ external_data.processed_at = timezone.now()
72
+ external_data.total_chunks = 0
73
+ external_data.total_tokens = 0
74
+ external_data.processing_cost = 0.0
75
+ external_data.save(update_fields=[
76
+ 'status', 'processed_at', 'total_chunks',
77
+ 'total_tokens', 'processing_cost'
78
+ ])
79
+ return {
80
+ 'success': True,
81
+ 'external_data_id': external_data_id,
82
+ 'total_chunks': 0,
83
+ 'total_tokens': 0,
84
+ 'processing_cost': 0.0,
85
+ 'processing_time': time.time() - start_time
86
+ }
87
+
88
+ logger.info(f"🚀 Starting external data processing: {external_data.title} (ID: {external_data_id})")
89
+
90
+ # Update status to processing
91
+ external_data.status = ExternalDataStatus.PROCESSING
92
+ external_data.processing_error = ""
93
+ external_data.save(update_fields=['status', 'processing_error'])
94
+
95
+ # Process external data using service (outside transaction for long-running operations)
96
+ service = ExternalDataService(external_data.user)
97
+ success = service.vectorize_external_data(external_data)
98
+
99
+ # Refresh from database to get updated statistics
100
+ external_data.refresh_from_db()
101
+
102
+ processing_time = time.time() - start_time
103
+
104
+ if success:
105
+ logger.info(
106
+ f"✅ External data processing completed: {external_data.title} "
107
+ f"({external_data.total_chunks} chunks, {external_data.total_tokens} tokens, "
108
+ f"${external_data.processing_cost:.6f}, {processing_time:.2f}s)"
109
+ )
110
+
111
+ return {
112
+ 'success': True,
113
+ 'external_data_id': external_data_id,
114
+ 'title': external_data.title,
115
+ 'total_chunks': external_data.total_chunks,
116
+ 'total_tokens': external_data.total_tokens,
117
+ 'processing_cost': external_data.processing_cost,
118
+ 'processing_time': processing_time
119
+ }
120
+ else:
121
+ logger.error(f"❌ External data processing failed: {external_data.title}")
122
+ return {
123
+ 'success': False,
124
+ 'external_data_id': external_data_id,
125
+ 'error': external_data.processing_error or 'Unknown processing error',
126
+ 'processing_time': processing_time
127
+ }
128
+
129
+ except ExternalData.DoesNotExist:
130
+ error_msg = f"External data {external_data_id} not found"
131
+ logger.error(f"❌ {error_msg}")
132
+ return {
133
+ 'success': False,
134
+ 'error': error_msg,
135
+ 'external_data_id': external_data_id
136
+ }
137
+
138
+ except Exception as e:
139
+ processing_time = time.time() - start_time
140
+ error_msg = f"Unexpected error processing external data {external_data_id}: {e}"
141
+ logger.error(f"❌ {error_msg}", exc_info=True)
142
+
143
+ # Try to update status to failed
144
+ try:
145
+ external_data = ExternalData.objects.get(id=external_data_id)
146
+ external_data.status = ExternalDataStatus.FAILED
147
+ external_data.processing_error = str(e)
148
+ external_data.save(update_fields=['status', 'processing_error'])
149
+ except Exception as save_error:
150
+ logger.error(f"❌ Failed to update external data status: {save_error}")
151
+
152
+ return {
153
+ 'success': False,
154
+ 'error': error_msg,
155
+ 'external_data_id': external_data_id,
156
+ 'processing_time': processing_time
157
+ }
158
+
159
+
160
+ @dramatiq.actor(
161
+ queue_name="knowledge",
162
+ max_retries=2,
163
+ min_backoff=5000, # 5 seconds
164
+ max_backoff=60000, # 60 seconds
165
+ priority=3
166
+ )
167
+ def bulk_process_external_data_async(
168
+ user_id: int,
169
+ external_data_ids: Optional[List[str]] = None,
170
+ force_reprocess: bool = False
171
+ ) -> Dict[str, Any]:
172
+ """
173
+ Process multiple external data sources asynchronously.
174
+
175
+ Args:
176
+ user_id: User ID to process external data for
177
+ external_data_ids: Specific external data IDs to process (None for all pending)
178
+ force_reprocess: Force reprocessing even if already completed
179
+
180
+ Returns:
181
+ Bulk processing results with statistics
182
+ """
183
+ start_time = time.time()
184
+
185
+ try:
186
+ from django.contrib.auth import get_user_model
187
+ User = get_user_model()
188
+ user = User.objects.get(id=user_id)
189
+
190
+ # Get external data to process
191
+ if external_data_ids:
192
+ external_data_queryset = ExternalData.objects.filter(
193
+ id__in=external_data_ids,
194
+ user=user
195
+ )
196
+ else:
197
+ # Process all pending external data for user
198
+ external_data_queryset = ExternalData.objects.filter(
199
+ user=user,
200
+ status=ExternalDataStatus.PENDING
201
+ )
202
+
203
+ external_data_list = list(external_data_queryset)
204
+ total_count = len(external_data_list)
205
+
206
+ if total_count == 0:
207
+ logger.info(f"No external data to process for user {user_id}")
208
+ return {
209
+ 'success': True,
210
+ 'user_id': user_id,
211
+ 'total_count': 0,
212
+ 'processed_count': 0,
213
+ 'failed_count': 0,
214
+ 'skipped_count': 0,
215
+ 'processing_time': time.time() - start_time
216
+ }
217
+
218
+ logger.info(f"🚀 Starting bulk external data processing for user {user_id}: {total_count} items")
219
+
220
+ # Process each external data
221
+ processed_count = 0
222
+ failed_count = 0
223
+ skipped_count = 0
224
+ results = []
225
+
226
+ for external_data in external_data_list:
227
+ try:
228
+ result = process_external_data_async.send(
229
+ str(external_data.id),
230
+ force_reprocess=force_reprocess
231
+ )
232
+
233
+ if result.get('success'):
234
+ if result.get('skipped'):
235
+ skipped_count += 1
236
+ else:
237
+ processed_count += 1
238
+ else:
239
+ failed_count += 1
240
+
241
+ results.append(result)
242
+
243
+ except Exception as e:
244
+ logger.error(f"❌ Failed to queue external data {external_data.id}: {e}")
245
+ failed_count += 1
246
+ results.append({
247
+ 'success': False,
248
+ 'external_data_id': str(external_data.id),
249
+ 'error': f'Failed to queue: {e}'
250
+ })
251
+
252
+ processing_time = time.time() - start_time
253
+
254
+ logger.info(
255
+ f"✅ Bulk external data processing queued for user {user_id}: "
256
+ f"{processed_count} processed, {failed_count} failed, "
257
+ f"{skipped_count} skipped out of {total_count} total ({processing_time:.2f}s)"
258
+ )
259
+
260
+ return {
261
+ 'success': True,
262
+ 'user_id': user_id,
263
+ 'total_count': total_count,
264
+ 'processed_count': processed_count,
265
+ 'failed_count': failed_count,
266
+ 'skipped_count': skipped_count,
267
+ 'processing_time': processing_time,
268
+ 'results': results
269
+ }
270
+
271
+ except Exception as e:
272
+ processing_time = time.time() - start_time
273
+ error_msg = f"Unexpected error in bulk external data processing for user {user_id}: {e}"
274
+ logger.error(f"❌ {error_msg}", exc_info=True)
275
+
276
+ return {
277
+ 'success': False,
278
+ 'error': error_msg,
279
+ 'user_id': user_id,
280
+ 'processing_time': processing_time
281
+ }
282
+
283
+
284
+ @dramatiq.actor(
285
+ queue_name="maintenance",
286
+ max_retries=1,
287
+ priority=1
288
+ )
289
+ def cleanup_failed_external_data_async(older_than_days: int = 7) -> Dict[str, Any]:
290
+ """
291
+ Clean up old failed external data processing attempts.
292
+
293
+ Args:
294
+ older_than_days: Remove failed external data older than this many days
295
+
296
+ Returns:
297
+ Cleanup results
298
+ """
299
+ start_time = time.time()
300
+
301
+ try:
302
+ from django.utils import timezone
303
+ from datetime import timedelta
304
+
305
+ cutoff_date = timezone.now() - timedelta(days=older_than_days)
306
+
307
+ # Find failed external data older than cutoff
308
+ failed_external_data = ExternalData.objects.filter(
309
+ status=ExternalDataStatus.FAILED,
310
+ updated_at__lt=cutoff_date
311
+ )
312
+
313
+ count = failed_external_data.count()
314
+
315
+ if count > 0:
316
+ logger.info(f"🧹 Cleaning up {count} failed external data older than {older_than_days} days")
317
+ deleted_count, _ = failed_external_data.delete()
318
+ logger.info(f"✅ Cleaned up {deleted_count} failed external data records")
319
+ else:
320
+ logger.info(f"No failed external data older than {older_than_days} days found")
321
+ deleted_count = 0
322
+
323
+ processing_time = time.time() - start_time
324
+
325
+ return {
326
+ 'success': True,
327
+ 'deleted_count': deleted_count,
328
+ 'older_than_days': older_than_days,
329
+ 'processing_time': processing_time
330
+ }
331
+
332
+ except Exception as e:
333
+ processing_time = time.time() - start_time
334
+ error_msg = f"Error cleaning up failed external data: {e}"
335
+ logger.error(f"❌ {error_msg}", exc_info=True)
336
+
337
+ return {
338
+ 'success': False,
339
+ 'error': error_msg,
340
+ 'processing_time': processing_time
341
+ }
@@ -0,0 +1,195 @@
1
+ """
2
+ Maintenance and cleanup tasks.
3
+ """
4
+
5
+ import dramatiq
6
+ import logging
7
+ from typing import Dict, Any
8
+ from django.db import connection
9
+ from django.utils import timezone
10
+ from datetime import timedelta
11
+ from django_cfg.modules.django_llm.llm.client import LLMClient
12
+ from django.core.cache import cache
13
+ from django.conf import settings
14
+
15
+ from ..models import Document, DocumentChunk, ProcessingStatus
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dramatiq.actor(
21
+ queue_name="knowledge",
22
+ max_retries=1,
23
+ priority=2
24
+ )
25
+ def cleanup_old_embeddings(days_old: int = 90) -> Dict[str, Any]:
26
+ """
27
+ Clean up old, unused embeddings and optimize storage.
28
+
29
+ Args:
30
+ days_old: Age threshold for cleanup
31
+
32
+ Returns:
33
+ Cleanup statistics
34
+ """
35
+ try:
36
+ cutoff_date = timezone.now() - timedelta(days=days_old)
37
+
38
+ # Find orphaned chunks (documents deleted but chunks remain)
39
+ orphaned_chunks = DocumentChunk.objects.filter(
40
+ document__isnull=True
41
+ )
42
+ orphaned_count = orphaned_chunks.count()
43
+ orphaned_chunks.delete()
44
+
45
+ # Find very old, unused chunks
46
+ old_chunks = DocumentChunk.objects.filter(
47
+ created_at__lt=cutoff_date,
48
+ document__processing_status=ProcessingStatus.FAILED
49
+ )
50
+ old_count = old_chunks.count()
51
+ old_chunks.delete()
52
+
53
+ # Vacuum and analyze tables
54
+ with connection.cursor() as cursor:
55
+ cursor.execute("VACUUM ANALYZE django_cfg_knowbase_document_chunks;")
56
+ cursor.execute("VACUUM ANALYZE django_cfg_knowbase_documents;")
57
+
58
+ result = {
59
+ "orphaned_chunks_deleted": orphaned_count,
60
+ "old_chunks_deleted": old_count,
61
+ "total_deleted": orphaned_count + old_count,
62
+ "cutoff_date": cutoff_date.isoformat(),
63
+ "timestamp": timezone.now().isoformat()
64
+ }
65
+
66
+ logger.info(f"Cleanup completed: {result}")
67
+ return result
68
+
69
+ except Exception as exc:
70
+ logger.error(f"Cleanup failed: {exc}")
71
+ raise
72
+
73
+
74
+ @dramatiq.actor(
75
+ queue_name="knowledge",
76
+ max_retries=1,
77
+ priority=1
78
+ )
79
+ def optimize_vector_indexes() -> Dict[str, Any]:
80
+ """
81
+ Optimize pgvector indexes for better performance.
82
+
83
+ Returns:
84
+ Optimization results
85
+ """
86
+ try:
87
+ with connection.cursor() as cursor:
88
+ # Reindex vector indexes (match model definition)
89
+ cursor.execute("REINDEX INDEX CONCURRENTLY embedding_cosine_idx;")
90
+
91
+ # Update table statistics
92
+ cursor.execute("ANALYZE django_cfg_knowbase_document_chunks;")
93
+
94
+ # Get index usage statistics
95
+ cursor.execute("""
96
+ SELECT
97
+ schemaname,
98
+ tablename,
99
+ indexname,
100
+ idx_tup_read,
101
+ idx_tup_fetch
102
+ FROM pg_stat_user_indexes
103
+ WHERE tablename = 'django_cfg_knowbase_document_chunks';
104
+ """)
105
+
106
+ index_stats = cursor.fetchall()
107
+
108
+ logger.info("Vector indexes optimized successfully")
109
+
110
+ return {
111
+ "status": "optimized",
112
+ "index_stats": index_stats,
113
+ "timestamp": timezone.now().isoformat()
114
+ }
115
+
116
+ except Exception as exc:
117
+ logger.error(f"Index optimization failed: {exc}")
118
+ raise
119
+
120
+
121
+ @dramatiq.actor(
122
+ queue_name="knowledge",
123
+ max_retries=1,
124
+ priority=1
125
+ )
126
+ def health_check_knowledge_base() -> Dict[str, Any]:
127
+ """
128
+ Perform health check on knowledge base components.
129
+
130
+ Returns:
131
+ Health check results
132
+ """
133
+ try:
134
+ health_status = {
135
+ "timestamp": timezone.now().isoformat(),
136
+ "database": "unknown",
137
+ "embeddings": "unknown",
138
+ "llm_service": "unknown",
139
+ "redis_cache": "unknown",
140
+ "statistics": {}
141
+ }
142
+
143
+ # Check database connectivity
144
+ try:
145
+ total_docs = Document.objects.count()
146
+ total_chunks = DocumentChunk.objects.count()
147
+
148
+ health_status["database"] = "healthy"
149
+ health_status["statistics"]["total_documents"] = total_docs
150
+ health_status["statistics"]["total_chunks"] = total_chunks
151
+ except Exception:
152
+ health_status["database"] = "unhealthy"
153
+
154
+ # Check LLM service
155
+ try:
156
+ llm_service = LLMClient(
157
+ apikey_openrouter=settings.api_keys.openrouter,
158
+ apikey_openai=settings.api_keys.openai
159
+ )
160
+ if hasattr(llm_service, 'is_configured') and llm_service.is_configured:
161
+ health_status["llm_service"] = "healthy"
162
+ elif settings.api_keys.openrouter:
163
+ health_status["llm_service"] = "healthy"
164
+ else:
165
+ health_status["llm_service"] = "not_configured"
166
+ except Exception:
167
+ health_status["llm_service"] = "unhealthy"
168
+
169
+ # Check Redis cache
170
+ try:
171
+ cache.set("health_check", "ok", 10)
172
+ if cache.get("health_check") == "ok":
173
+ health_status["redis_cache"] = "healthy"
174
+ else:
175
+ health_status["redis_cache"] = "unhealthy"
176
+ except Exception:
177
+ health_status["redis_cache"] = "unhealthy"
178
+
179
+ # Overall health
180
+ health_status["overall"] = (
181
+ "healthy" if all(
182
+ status == "healthy" for status in [
183
+ health_status["database"],
184
+ health_status["llm_service"],
185
+ health_status["redis_cache"]
186
+ ]
187
+ ) else "degraded"
188
+ )
189
+
190
+ logger.info(f"Health check completed: {health_status['overall']}")
191
+ return health_status
192
+
193
+ except Exception as exc:
194
+ logger.error(f"Health check failed: {exc}")
195
+ raise
@@ -0,0 +1,43 @@
1
+ """
2
+ Knowledge Base URL Configuration
3
+ """
4
+
5
+ from django.urls import path, include
6
+ from rest_framework.routers import DefaultRouter
7
+ from .views import (
8
+ DocumentViewSet, ChatViewSet, ChatSessionViewSet,
9
+ DocumentArchiveViewSet, ArchiveItemViewSet, ArchiveItemChunkViewSet
10
+ )
11
+ from .views.public_views import PublicDocumentViewSet, PublicCategoryViewSet
12
+
13
+ # Create router and register viewsets
14
+ router = DefaultRouter()
15
+ router.register(r'documents', DocumentViewSet, basename='document')
16
+ router.register(r'chat', ChatViewSet, basename='chat')
17
+ router.register(r'sessions', ChatSessionViewSet, basename='session')
18
+
19
+ # Archive router for authenticated users
20
+ archive_router = DefaultRouter()
21
+ archive_router.register(r'archives', DocumentArchiveViewSet, basename='archive')
22
+ archive_router.register(r'items', ArchiveItemViewSet, basename='archive-item')
23
+ archive_router.register(r'chunks', ArchiveItemChunkViewSet, basename='archive-chunk')
24
+
25
+ # Public router for client access
26
+ public_router = DefaultRouter()
27
+ public_router.register(r'documents', PublicDocumentViewSet, basename='public-document')
28
+ public_router.register(r'categories', PublicCategoryViewSet, basename='public-category')
29
+
30
+ # URL patterns
31
+ urlpatterns = [
32
+ # Admin API endpoints (require authentication + admin rights)
33
+ path('admin/', include(router.urls)),
34
+
35
+ # Archive API endpoints (require authentication)
36
+ path('', include(archive_router.urls)),
37
+
38
+ # Public API endpoints (no authentication required)
39
+ path('public/', include(public_router.urls)),
40
+ ]
41
+
42
+ # Add app name for namespacing
43
+ app_name = 'knowbase'
@@ -0,0 +1,12 @@
1
+ """
2
+ Knowledge Base Utilities
3
+
4
+ Helper functions and classes for text processing and utilities.
5
+ """
6
+
7
+ from .text_processing import *
8
+
9
+ __all__ = [
10
+ 'TextProcessor',
11
+ 'SemanticChunker',
12
+ ]