django-cfg 1.1.82__py3-none-any.whl โ†’ 1.2.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 (244) 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 +450 -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 +91 -19
  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/twilio_service.py +2 -2
  198. django_cfg/modules/django_unfold/__init__.py +69 -0
  199. django_cfg/modules/{unfold โ†’ django_unfold}/callbacks.py +23 -22
  200. django_cfg/modules/django_unfold/dashboard.py +278 -0
  201. django_cfg/modules/django_unfold/icons/README.md +145 -0
  202. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  203. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  204. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  205. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  206. django_cfg/modules/django_unfold/models/config.py +601 -0
  207. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  208. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  209. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  210. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  211. django_cfg/modules/{unfold โ†’ django_unfold}/system_monitor.py +2 -2
  212. django_cfg/modules/django_unfold/utils.py +140 -0
  213. django_cfg/registry/__init__.py +23 -0
  214. django_cfg/registry/core.py +61 -0
  215. django_cfg/registry/exceptions.py +11 -0
  216. django_cfg/registry/modules.py +12 -0
  217. django_cfg/registry/services.py +26 -0
  218. django_cfg/registry/third_party.py +52 -0
  219. django_cfg/routing/__init__.py +19 -0
  220. django_cfg/routing/callbacks.py +198 -0
  221. django_cfg/routing/routers.py +48 -0
  222. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  223. django_cfg/templatetags/__init__.py +0 -0
  224. django_cfg/templatetags/django_cfg.py +33 -0
  225. django_cfg/urls.py +33 -0
  226. django_cfg/utils/path_resolution.py +1 -1
  227. django_cfg/utils/smart_defaults.py +7 -61
  228. django_cfg/utils/toolkit.py +663 -0
  229. {django_cfg-1.1.82.dist-info โ†’ django_cfg-1.2.1.dist-info}/METADATA +83 -86
  230. django_cfg-1.2.1.dist-info/RECORD +441 -0
  231. django_cfg/archive/django_sample.zip +0 -0
  232. django_cfg/models/unfold.py +0 -271
  233. django_cfg/modules/unfold/__init__.py +0 -29
  234. django_cfg/modules/unfold/dashboard.py +0 -318
  235. django_cfg/pyproject.toml +0 -370
  236. django_cfg/routers.py +0 -83
  237. django_cfg-1.1.82.dist-info/RECORD +0 -278
  238. /django_cfg/{exceptions.py โ†’ core/exceptions.py} +0 -0
  239. /django_cfg/modules/{unfold โ†’ django_unfold}/models.py +0 -0
  240. /django_cfg/modules/{unfold โ†’ django_unfold}/tailwind.py +0 -0
  241. /django_cfg/{version_check.py โ†’ utils/version_check.py} +0 -0
  242. {django_cfg-1.1.82.dist-info โ†’ django_cfg-1.2.1.dist-info}/WHEEL +0 -0
  243. {django_cfg-1.1.82.dist-info โ†’ django_cfg-1.2.1.dist-info}/entry_points.txt +0 -0
  244. {django_cfg-1.1.82.dist-info โ†’ django_cfg-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,211 @@
1
+ """
2
+ Archive processing signals.
3
+ """
4
+
5
+ from django.db.models.signals import post_save, post_delete
6
+ from django.dispatch import receiver
7
+ from django.core.cache import cache
8
+ import logging
9
+
10
+ from ..models import DocumentArchive, ArchiveItem, ArchiveItemChunk
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @receiver(post_save, sender=DocumentArchive)
16
+ def archive_post_save(sender, instance, created, **kwargs):
17
+ """Handle archive creation and updates."""
18
+
19
+ # Clear user's archive cache on any archive change
20
+ cache_key = f"user_archives:{instance.user.id}"
21
+ cache.delete(cache_key)
22
+
23
+ if created and instance.archive_file:
24
+ # New archive with file - start processing
25
+ logger.info(f"๐Ÿ“ฆ New archive created: {instance.title} (ID: {instance.id})")
26
+ _start_archive_processing(instance)
27
+
28
+ elif not created:
29
+ # Archive update - check what changed
30
+ update_fields = kwargs.get('update_fields')
31
+
32
+ # Define fields that, if updated alone, should NOT trigger reprocessing
33
+ processing_fields = {
34
+ 'processing_status', 'processed_at', 'processing_duration_ms',
35
+ 'processing_error', 'total_items', 'processed_items', 'total_chunks',
36
+ 'vectorized_chunks', 'total_tokens', 'total_cost_usd'
37
+ }
38
+
39
+ # If specific fields were updated, check if they are only processing-related
40
+ if update_fields is not None:
41
+ update_fields_set = set(update_fields) if update_fields else set()
42
+
43
+ if update_fields_set and update_fields_set.issubset(processing_fields):
44
+ # This is just a processing status update - don't reprocess
45
+ logger.debug(f"๐Ÿ“Š Archive stats updated: {instance.title}")
46
+ return
47
+
48
+ # Check if archive file was updated
49
+ if 'archive_file' in update_fields_set and instance.archive_file:
50
+ logger.info(f"๐Ÿ“ฆ Archive file updated: {instance.title} (ID: {instance.id})")
51
+
52
+ # Clear existing items and reset processing state
53
+ _reset_archive_processing(instance)
54
+
55
+ # Start new processing
56
+ _start_archive_processing(instance)
57
+ else:
58
+ logger.debug(f"๐Ÿ“Š Archive non-file update: {instance.title} (fields: {update_fields_set})")
59
+ else:
60
+ # Full save without update_fields - check if file exists and process if needed
61
+ if instance.archive_file and instance.processing_status == 'pending':
62
+ logger.info(f"๐Ÿ“ฆ Archive saved, checking if processing needed: {instance.title}")
63
+ _start_archive_processing(instance)
64
+
65
+
66
+ def _reset_archive_processing(archive):
67
+ """Reset archive processing state and clear items."""
68
+ logger.debug(f"๐Ÿงน Clearing items for archive: {archive.title}")
69
+
70
+ # Delete existing items (cascades to chunks)
71
+ archive.items.all().delete()
72
+
73
+ # Reset processing fields
74
+ archive.processing_status = "pending"
75
+ archive.processed_at = None
76
+ archive.processing_duration_ms = 0
77
+ archive.processing_error = ""
78
+ archive.total_items = 0
79
+ archive.processed_items = 0
80
+ archive.total_chunks = 0
81
+ archive.vectorized_chunks = 0
82
+ archive.total_tokens = 0
83
+ archive.total_cost_usd = 0
84
+
85
+ # Save with explicit update_fields to avoid triggering this signal again
86
+ archive.save(update_fields=[
87
+ 'processing_status', 'processed_at', 'processing_duration_ms',
88
+ 'processing_error', 'total_items', 'processed_items', 'total_chunks',
89
+ 'vectorized_chunks', 'total_tokens', 'total_cost_usd'
90
+ ])
91
+
92
+
93
+ def _start_archive_processing(archive):
94
+ """Start async archive processing."""
95
+ try:
96
+ # Lazy import to avoid middleware initialization issues
97
+ from ..tasks.archive_tasks import process_archive_task
98
+ process_archive_task.send(str(archive.id), str(archive.user.id))
99
+ logger.info(f"๐Ÿš€ Started async processing for archive: {archive.id}")
100
+
101
+ except Exception as e:
102
+ logger.error(f"โŒ Failed to start archive processing: {e}")
103
+
104
+ # Update archive status to failed
105
+ archive.processing_status = "failed"
106
+ archive.processing_error = f"Failed to start processing: {e}"
107
+ archive.save(update_fields=['processing_status', 'processing_error'])
108
+
109
+
110
+ @receiver(post_save, sender=ArchiveItem)
111
+ def archive_item_post_save(sender, instance, created, **kwargs):
112
+ """Handle archive item creation."""
113
+ if created:
114
+ logger.debug(f"๐Ÿ“„ New archive item created: {instance.archive.title} - {instance.item_name}")
115
+
116
+ # Update archive item count
117
+ archive = instance.archive
118
+ archive.total_items = archive.items.count()
119
+ archive.save(update_fields=['total_items'])
120
+
121
+
122
+ @receiver(post_delete, sender=ArchiveItem)
123
+ def archive_item_post_delete(sender, instance, **kwargs):
124
+ """Handle archive item deletion."""
125
+ try:
126
+ # Safely get archive - it might be deleted already due to cascade
127
+ try:
128
+ archive = instance.archive
129
+ archive_title = archive.title
130
+ except (AttributeError, DocumentArchive.DoesNotExist):
131
+ logger.debug("โš ๏ธ Archive already deleted, skipping item count update")
132
+ return
133
+
134
+ logger.debug(f"๐Ÿ—‘๏ธ Archive item deleted: {archive_title} - {instance.item_name}")
135
+
136
+ # Update archive item count
137
+ archive.total_items = archive.items.count()
138
+ archive.save(update_fields=['total_items'])
139
+
140
+ except DocumentArchive.DoesNotExist:
141
+ # Archive was already deleted
142
+ logger.debug("Archive already deleted, skipping item count update")
143
+ except Exception as e:
144
+ logger.error(f"โŒ Error in archive item post-delete signal: {e}")
145
+ # Don't re-raise to avoid breaking deletion
146
+
147
+
148
+ @receiver(post_save, sender=ArchiveItemChunk)
149
+ def archive_chunk_post_save(sender, instance, created, **kwargs):
150
+ """Handle archive chunk creation."""
151
+ if created:
152
+ logger.debug(f"๐Ÿงฉ New archive chunk created: {instance.item.item_name} chunk {instance.chunk_index}")
153
+
154
+ # Update archive chunk count
155
+ archive = instance.archive
156
+ archive.total_chunks = ArchiveItemChunk.objects.filter(archive=archive).count()
157
+
158
+ # Update vectorized chunks count
159
+ archive.vectorized_chunks = ArchiveItemChunk.objects.filter(
160
+ archive=archive,
161
+ embedding__isnull=False
162
+ ).count()
163
+
164
+ archive.save(update_fields=['total_chunks', 'vectorized_chunks'])
165
+
166
+
167
+ @receiver(post_delete, sender=ArchiveItemChunk)
168
+ def archive_chunk_post_delete(sender, instance, **kwargs):
169
+ """Handle archive chunk deletion."""
170
+ try:
171
+ # Safely get item name - item might be deleted already due to cascade
172
+ try:
173
+ item_name = instance.item.item_name if hasattr(instance, 'item') and instance.item else "unknown"
174
+ except (AttributeError, ArchiveItem.DoesNotExist):
175
+ item_name = "unknown"
176
+
177
+ logger.debug(f"๐Ÿ—‘๏ธ Archive chunk deleted: {item_name} chunk {instance.chunk_index}")
178
+
179
+ # Update archive chunk counts - get archive through item if available
180
+ try:
181
+ if hasattr(instance, 'item') and instance.item:
182
+ archive = instance.item.archive
183
+ else:
184
+ # If item is already deleted, we can't update counts safely
185
+ logger.debug("โš ๏ธ Cannot update chunk counts - parent item already deleted")
186
+ return
187
+
188
+ archive.total_chunks = ArchiveItemChunk.objects.filter(item__archive=archive).count()
189
+ archive.vectorized_chunks = ArchiveItemChunk.objects.filter(
190
+ item__archive=archive,
191
+ embedding__isnull=False
192
+ ).count()
193
+
194
+ archive.save(update_fields=['total_chunks', 'vectorized_chunks'])
195
+
196
+ except (AttributeError, ArchiveItem.DoesNotExist, DocumentArchive.DoesNotExist):
197
+ logger.debug("โš ๏ธ Cannot update chunk counts - related objects already deleted")
198
+
199
+ except Exception as e:
200
+ logger.error(f"โŒ Error in archive chunk post-delete signal: {e}")
201
+ # Don't re-raise to avoid breaking deletion
202
+
203
+
204
+ @receiver(post_delete, sender=DocumentArchive)
205
+ def archive_post_delete(sender, instance, **kwargs):
206
+ """Handle archive deletion cleanup."""
207
+ # Clear user's archive cache
208
+ cache_key = f"user_archives:{instance.user.id}"
209
+ cache.delete(cache_key)
210
+
211
+ logger.info(f"๐Ÿ—‘๏ธ Archive deleted: {instance.title} (ID: {instance.id})")
@@ -0,0 +1,37 @@
1
+ """
2
+ Chat and messaging related signals.
3
+ """
4
+
5
+ from django.db.models.signals import post_save, post_delete
6
+ from django.db import models
7
+ from django.dispatch import receiver
8
+ import logging
9
+
10
+ from ..models import ChatSession, ChatMessage
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @receiver(post_save, sender=ChatMessage)
16
+ def message_post_save(sender, instance, created, **kwargs):
17
+ """Handle chat message creation."""
18
+ if created:
19
+ logger.debug(f"๐Ÿ’ฌ New message: {instance.session.title} - {instance.role}")
20
+
21
+ # Update session statistics
22
+ session = instance.session
23
+ session.messages_count = session.messages.count()
24
+ session.total_tokens_used = session.messages.aggregate(
25
+ total=models.Sum('tokens_used')
26
+ )['total'] or 0
27
+ session.total_cost_usd = session.messages.aggregate(
28
+ total=models.Sum('cost_usd')
29
+ )['total'] or 0
30
+
31
+ session.save(update_fields=['messages_count', 'total_tokens_used', 'total_cost_usd'])
32
+
33
+
34
+ @receiver(post_delete, sender=ChatSession)
35
+ def session_post_delete(sender, instance, **kwargs):
36
+ """Handle chat session deletion."""
37
+ logger.info(f"๐Ÿ—‘๏ธ Chat session deleted: {instance.title} (ID: {instance.id})")
@@ -0,0 +1,143 @@
1
+ """
2
+ Document and DocumentChunk related signals.
3
+ """
4
+
5
+ from django.db.models.signals import post_save, post_delete
6
+ from django.db import models
7
+ from django.dispatch import receiver
8
+ from django.core.cache import cache
9
+ import logging
10
+
11
+ from ..models import Document, DocumentChunk, ProcessingStatus
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @receiver(post_save, sender=Document)
17
+ def document_post_save(sender, instance, created, **kwargs):
18
+ """Handle document creation and updates."""
19
+
20
+ # Clear user's document cache on any document change
21
+ cache_key = f"user_documents:{instance.user.id}"
22
+ cache.delete(cache_key)
23
+
24
+ if created:
25
+ # New document - always process
26
+ logger.info(f"๐Ÿ“„ New document created: {instance.title} (ID: {instance.id})")
27
+ _start_document_processing(instance)
28
+
29
+ else:
30
+ # Document update - check what changed
31
+ update_fields = kwargs.get('update_fields')
32
+
33
+ # Define fields that, if updated alone, should NOT trigger reprocessing
34
+ processing_fields = {
35
+ 'processing_status', 'processing_started_at', 'processing_completed_at',
36
+ 'processing_error', 'chunks_count', 'total_tokens', 'total_cost_usd'
37
+ }
38
+
39
+ # If specific fields were updated, check if they are only processing-related
40
+ if update_fields is not None:
41
+ update_fields_set = set(update_fields) if update_fields else set()
42
+
43
+ if update_fields_set and update_fields_set.issubset(processing_fields):
44
+ # This is just a processing status update - don't reprocess
45
+ logger.debug(f"๐Ÿ“Š Document stats updated: {instance.title}")
46
+ return
47
+
48
+ # Check if content actually changed
49
+ content_fields = {'content', 'content_hash'}
50
+ if content_fields.intersection(update_fields_set):
51
+ logger.info(f"๐Ÿ“ Document content updated: {instance.title} (ID: {instance.id})")
52
+
53
+ # Clear existing chunks and reset processing state
54
+ _reset_document_processing(instance)
55
+
56
+ # Start new processing
57
+ _start_document_processing(instance)
58
+ else:
59
+ logger.debug(f"๐Ÿ“Š Document non-content update: {instance.title} (fields: {update_fields_set})")
60
+ else:
61
+ # Full save without update_fields specified (e.g., from admin save button or manual save())
62
+ # Assume content might have changed and reprocess
63
+ logger.info(f"๐Ÿ“ Document saved without specific fields, assuming content update: {instance.title} (ID: {instance.id})")
64
+ _reset_document_processing(instance)
65
+ _start_document_processing(instance)
66
+
67
+
68
+ def _reset_document_processing(document):
69
+ """Reset document processing state and clear chunks."""
70
+ logger.debug(f"๐Ÿงน Clearing chunks for document: {document.title}")
71
+
72
+ # Delete existing chunks
73
+ document.chunks.all().delete()
74
+
75
+ # Reset processing fields
76
+ document.processing_status = "pending"
77
+ document.processing_started_at = None
78
+ document.processing_completed_at = None
79
+ document.processing_error = ""
80
+ document.chunks_count = 0
81
+ document.total_tokens = 0
82
+ document.total_cost_usd = 0
83
+
84
+ # Save with explicit update_fields to avoid triggering this signal again
85
+ document.save(update_fields=[
86
+ 'processing_status', 'processing_started_at', 'processing_completed_at',
87
+ 'processing_error', 'chunks_count', 'total_tokens', 'total_cost_usd'
88
+ ])
89
+
90
+
91
+ def _start_document_processing(document):
92
+ """Start async document processing."""
93
+ try:
94
+ # Lazy import to avoid middleware initialization issues
95
+ from ..tasks.document_processing import process_document_async
96
+ process_document_async.send(str(document.id))
97
+ logger.info(f"๐Ÿš€ Started async processing for document: {document.id}")
98
+
99
+ except Exception as e:
100
+ logger.error(f"โŒ Failed to start document processing: {e}")
101
+
102
+ # Update document status to failed
103
+ document.processing_status = ProcessingStatus.FAILED
104
+ document.processing_error = f"Failed to start processing: {e}"
105
+ document.save(update_fields=['processing_status', 'processing_error'])
106
+
107
+
108
+ @receiver(post_save, sender=DocumentChunk)
109
+ def chunk_post_save(sender, instance, created, **kwargs):
110
+ """Handle chunk creation."""
111
+ if created:
112
+ logger.debug(f"๐Ÿงฉ New chunk created: {instance.document.title} chunk {instance.chunk_index}")
113
+
114
+ # Update document chunk count (this will trigger document save with update_fields)
115
+ document = instance.document
116
+ document.chunks_count = document.chunks.count()
117
+ document.save(update_fields=['chunks_count'])
118
+
119
+
120
+ @receiver(post_delete, sender=DocumentChunk)
121
+ def chunk_post_delete(sender, instance, **kwargs):
122
+ """Handle chunk deletion."""
123
+ try:
124
+ logger.debug(f"๐Ÿ—‘๏ธ Chunk deleted: {instance.document.title} chunk {instance.chunk_index}")
125
+
126
+ # Update document chunk count
127
+ document = instance.document
128
+ document.chunks_count = document.chunks.count()
129
+ document.save(update_fields=['chunks_count'])
130
+
131
+ except Document.DoesNotExist:
132
+ # Document was already deleted
133
+ logger.debug("Document already deleted, skipping chunk count update")
134
+
135
+
136
+ @receiver(post_delete, sender=Document)
137
+ def document_post_delete(sender, instance, **kwargs):
138
+ """Handle document deletion cleanup."""
139
+ # Clear user's document cache
140
+ cache_key = f"user_documents:{instance.user.id}"
141
+ cache.delete(cache_key)
142
+
143
+ logger.info(f"๐Ÿ—‘๏ธ Document deleted: {instance.title} (ID: {instance.id})")
@@ -0,0 +1,157 @@
1
+ """
2
+ External Data and ExternalDataChunk related signals.
3
+ """
4
+
5
+ from django.db.models.signals import post_save, post_delete
6
+ from django.db import models
7
+ from django.dispatch import receiver
8
+ from django.core.cache import cache
9
+ import logging
10
+
11
+ from ..models.external_data import ExternalData, ExternalDataChunk, ExternalDataStatus
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @receiver(post_save, sender=ExternalData)
17
+ def external_data_post_save(sender, instance, created, **kwargs):
18
+ """Handle external data creation and updates."""
19
+
20
+ # Clear user's external data cache on any change
21
+ cache_key = f"user_external_data:{instance.user.id}"
22
+ cache.delete(cache_key)
23
+
24
+ if created:
25
+ # New external data - process if has content
26
+ logger.info(f"๐Ÿ”— New external data created: {instance.title} (ID: {instance.id})")
27
+ if instance.content and instance.content.strip():
28
+ _start_external_data_processing(instance)
29
+ else:
30
+ logger.debug(f"๐Ÿ“ External data created without content: {instance.title}")
31
+
32
+ else:
33
+ # External data update - check what changed
34
+ update_fields = kwargs.get('update_fields')
35
+
36
+ # Define fields that, if updated alone, should NOT trigger reprocessing
37
+ processing_fields = {
38
+ 'status', 'processed_at', 'source_updated_at', 'processing_error',
39
+ 'total_chunks', 'total_tokens', 'processing_cost'
40
+ }
41
+
42
+ # If specific fields were updated, check if they are only processing-related
43
+ if update_fields is not None:
44
+ update_fields_set = set(update_fields) if update_fields else set()
45
+
46
+ if update_fields_set and update_fields_set.issubset(processing_fields):
47
+ # This is just a processing status update - don't reprocess
48
+ logger.debug(f"๐Ÿ“Š External data stats updated: {instance.title}")
49
+ return
50
+
51
+ # Check if content actually changed
52
+ content_fields = {'content', 'content_hash', 'source_config', 'chunk_size', 'overlap_size', 'embedding_model'}
53
+ if content_fields.intersection(update_fields_set):
54
+ logger.info(f"๐Ÿ“ External data content updated: {instance.title} (ID: {instance.id})")
55
+
56
+ # Only reprocess if there's content
57
+ if instance.content and instance.content.strip():
58
+ # Clear existing chunks and reset processing state
59
+ _reset_external_data_processing(instance)
60
+
61
+ # Start new processing
62
+ _start_external_data_processing(instance)
63
+ else:
64
+ logger.debug(f"๐Ÿ“ External data updated but no content: {instance.title}")
65
+ else:
66
+ logger.debug(f"๐Ÿ“Š External data non-content update: {instance.title} (fields: {update_fields_set})")
67
+ else:
68
+ # Full save without update_fields - check if content exists and processing is needed
69
+ if instance.content and instance.content.strip():
70
+ # Check if content hash changed (indicating content update)
71
+ if hasattr(instance, '_original_content_hash'):
72
+ if instance.content_hash != instance._original_content_hash:
73
+ logger.info(f"๐Ÿ”ฎ External data content changed (hash mismatch), reprocessing: {instance.title}")
74
+ _reset_external_data_processing(instance)
75
+ _start_external_data_processing(instance)
76
+ else:
77
+ logger.debug(f"๐Ÿ“Š External data saved but content unchanged: {instance.title}")
78
+ elif instance.status == ExternalDataStatus.PENDING:
79
+ logger.info(f"๐Ÿ”ฎ External data saved, checking if processing needed: {instance.title}")
80
+ _start_external_data_processing(instance)
81
+
82
+
83
+ def _reset_external_data_processing(external_data):
84
+ """Reset external data processing state and clear chunks."""
85
+ logger.debug(f"๐Ÿงน Clearing chunks for external data: {external_data.title}")
86
+
87
+ # Delete existing chunks
88
+ external_data.chunks.all().delete()
89
+
90
+ # Reset processing fields
91
+ external_data.status = ExternalDataStatus.PENDING
92
+ external_data.processed_at = None
93
+ external_data.processing_error = ""
94
+ external_data.total_chunks = 0
95
+ external_data.total_tokens = 0
96
+ external_data.processing_cost = 0.0
97
+
98
+ # Save with explicit update_fields to avoid triggering this signal again
99
+ external_data.save(update_fields=[
100
+ 'status', 'processed_at', 'processing_error',
101
+ 'total_chunks', 'total_tokens', 'processing_cost'
102
+ ])
103
+
104
+
105
+ def _start_external_data_processing(external_data):
106
+ """Start async external data processing."""
107
+ try:
108
+ # Lazy import to avoid middleware initialization issues
109
+ from ..tasks.external_data_tasks import process_external_data_async
110
+ process_external_data_async.send(str(external_data.id))
111
+ logger.info(f"๐Ÿš€ Started async processing for external data: {external_data.id}")
112
+
113
+ except Exception as e:
114
+ logger.error(f"โŒ Failed to start external data processing: {e}")
115
+
116
+ # Update external data status to failed
117
+ external_data.status = ExternalDataStatus.FAILED
118
+ external_data.processing_error = f"Failed to start processing: {e}"
119
+ external_data.save(update_fields=['status', 'processing_error'])
120
+
121
+
122
+ @receiver(post_save, sender=ExternalDataChunk)
123
+ def external_data_chunk_post_save(sender, instance, created, **kwargs):
124
+ """Handle external data chunk creation."""
125
+ if created:
126
+ logger.debug(f"๐Ÿงฉ New external data chunk created: {instance.external_data.title} chunk {instance.chunk_index}")
127
+
128
+ # Update external data chunk count (this will trigger external data save with update_fields)
129
+ external_data = instance.external_data
130
+ external_data.total_chunks = external_data.chunks.count()
131
+ external_data.save(update_fields=['total_chunks'])
132
+
133
+
134
+ @receiver(post_delete, sender=ExternalDataChunk)
135
+ def external_data_chunk_post_delete(sender, instance, **kwargs):
136
+ """Handle external data chunk deletion."""
137
+ try:
138
+ logger.debug(f"๐Ÿ—‘๏ธ External data chunk deleted: {instance.external_data.title} chunk {instance.chunk_index}")
139
+
140
+ # Update external data chunk count
141
+ external_data = instance.external_data
142
+ external_data.total_chunks = external_data.chunks.count()
143
+ external_data.save(update_fields=['total_chunks'])
144
+
145
+ except ExternalData.DoesNotExist:
146
+ # External data was already deleted
147
+ logger.debug("External data already deleted, skipping chunk count update")
148
+
149
+
150
+ @receiver(post_delete, sender=ExternalData)
151
+ def external_data_post_delete(sender, instance, **kwargs):
152
+ """Handle external data deletion cleanup."""
153
+ # Clear user's external data cache
154
+ cache_key = f"user_external_data:{instance.user.id}"
155
+ cache.delete(cache_key)
156
+
157
+ logger.info(f"๐Ÿ—‘๏ธ External data deleted: {instance.title} (ID: {instance.id})")
@@ -0,0 +1,39 @@
1
+ """
2
+ Knowledge Base Background Tasks
3
+
4
+ Dramatiq tasks for document processing and maintenance.
5
+ """
6
+
7
+ from .document_processing import *
8
+ from .maintenance import *
9
+ from .archive_tasks import *
10
+ from .external_data_tasks import *
11
+
12
+
13
+ __all__ = [
14
+ # Document processing
15
+ 'process_document_async',
16
+ 'reprocess_document_chunks',
17
+ 'generate_embeddings_batch',
18
+
19
+ # Archive processing
20
+ 'process_archive_task',
21
+ 'vectorize_archive_items_task',
22
+ 'cleanup_failed_archives_task',
23
+ 'generate_archive_statistics_task',
24
+ 'archive_health_check_task',
25
+ 'test_archive_task',
26
+
27
+ # External data processing
28
+ 'process_external_data_async',
29
+ 'bulk_process_external_data_async',
30
+ 'cleanup_failed_external_data_async',
31
+
32
+ # Maintenance
33
+ 'cleanup_old_embeddings',
34
+ 'optimize_vector_indexes',
35
+ 'health_check_knowledge_base',
36
+
37
+ # Test tasks
38
+ 'test_simple_task',
39
+ ]