django-cfg 1.1.82__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.
- django_cfg/__init__.py +20 -448
- django_cfg/apps/accounts/README.md +3 -3
- django_cfg/apps/accounts/admin/__init__.py +0 -2
- django_cfg/apps/accounts/admin/activity.py +2 -9
- django_cfg/apps/accounts/admin/filters.py +0 -42
- django_cfg/apps/accounts/admin/inlines.py +8 -8
- django_cfg/apps/accounts/admin/otp.py +5 -5
- django_cfg/apps/accounts/admin/registration_source.py +1 -8
- django_cfg/apps/accounts/admin/user.py +12 -20
- django_cfg/apps/accounts/managers/user_manager.py +2 -129
- django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
- django_cfg/apps/accounts/models.py +3 -123
- django_cfg/apps/accounts/serializers/otp.py +40 -44
- django_cfg/apps/accounts/serializers/profile.py +0 -2
- django_cfg/apps/accounts/services/otp_service.py +98 -186
- django_cfg/apps/accounts/signals.py +25 -15
- django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
- django_cfg/apps/accounts/views/otp.py +35 -36
- django_cfg/apps/agents/README.md +129 -0
- django_cfg/apps/agents/__init__.py +68 -0
- django_cfg/apps/agents/admin/__init__.py +17 -0
- django_cfg/apps/agents/admin/execution_admin.py +460 -0
- django_cfg/apps/agents/admin/registry_admin.py +360 -0
- django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
- django_cfg/apps/agents/apps.py +29 -0
- django_cfg/apps/agents/core/__init__.py +20 -0
- django_cfg/apps/agents/core/agent.py +281 -0
- django_cfg/apps/agents/core/dependencies.py +154 -0
- django_cfg/apps/agents/core/exceptions.py +66 -0
- django_cfg/apps/agents/core/models.py +106 -0
- django_cfg/apps/agents/core/orchestrator.py +391 -0
- django_cfg/apps/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- django_cfg/apps/agents/integration/__init__.py +14 -0
- django_cfg/apps/agents/integration/middleware.py +80 -0
- django_cfg/apps/agents/integration/registry.py +345 -0
- django_cfg/apps/agents/integration/signals.py +50 -0
- django_cfg/apps/agents/management/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/create_agent.py +365 -0
- django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
- django_cfg/apps/agents/managers/__init__.py +23 -0
- django_cfg/apps/agents/managers/execution.py +236 -0
- django_cfg/apps/agents/managers/registry.py +254 -0
- django_cfg/apps/agents/managers/toolsets.py +496 -0
- django_cfg/apps/agents/migrations/0001_initial.py +286 -0
- django_cfg/apps/agents/migrations/__init__.py +5 -0
- django_cfg/apps/agents/models/__init__.py +15 -0
- django_cfg/apps/agents/models/execution.py +215 -0
- django_cfg/apps/agents/models/registry.py +220 -0
- django_cfg/apps/agents/models/toolsets.py +305 -0
- django_cfg/apps/agents/patterns/__init__.py +24 -0
- django_cfg/apps/agents/patterns/content_agents.py +234 -0
- django_cfg/apps/agents/toolsets/__init__.py +15 -0
- django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
- django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
- django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
- django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
- django_cfg/apps/agents/urls.py +46 -0
- django_cfg/apps/knowbase/README.md +150 -0
- django_cfg/apps/knowbase/__init__.py +27 -0
- django_cfg/apps/knowbase/admin/__init__.py +23 -0
- django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
- django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
- django_cfg/apps/knowbase/admin/document_admin.py +650 -0
- django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
- django_cfg/apps/knowbase/apps.py +81 -0
- django_cfg/apps/knowbase/config/README.md +176 -0
- django_cfg/apps/knowbase/config/__init__.py +51 -0
- django_cfg/apps/knowbase/config/constance_fields.py +186 -0
- django_cfg/apps/knowbase/config/constance_settings.py +200 -0
- django_cfg/apps/knowbase/config/settings.py +444 -0
- django_cfg/apps/knowbase/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- django_cfg/apps/knowbase/management/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
- django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
- django_cfg/apps/knowbase/managers/__init__.py +22 -0
- django_cfg/apps/knowbase/managers/archive.py +426 -0
- django_cfg/apps/knowbase/managers/base.py +32 -0
- django_cfg/apps/knowbase/managers/chat.py +141 -0
- django_cfg/apps/knowbase/managers/document.py +203 -0
- django_cfg/apps/knowbase/managers/external_data.py +471 -0
- django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
- django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
- django_cfg/apps/knowbase/migrations/__init__.py +5 -0
- django_cfg/apps/knowbase/mixins/__init__.py +15 -0
- django_cfg/apps/knowbase/mixins/config.py +108 -0
- django_cfg/apps/knowbase/mixins/creator.py +81 -0
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
- django_cfg/apps/knowbase/mixins/service.py +362 -0
- django_cfg/apps/knowbase/models/__init__.py +41 -0
- django_cfg/apps/knowbase/models/archive.py +599 -0
- django_cfg/apps/knowbase/models/base.py +58 -0
- django_cfg/apps/knowbase/models/chat.py +157 -0
- django_cfg/apps/knowbase/models/document.py +267 -0
- django_cfg/apps/knowbase/models/external_data.py +376 -0
- django_cfg/apps/knowbase/serializers/__init__.py +68 -0
- django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
- django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
- django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
- django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
- django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
- django_cfg/apps/knowbase/services/__init__.py +40 -0
- django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
- django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
- django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
- django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
- django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
- django_cfg/apps/knowbase/services/base.py +53 -0
- django_cfg/apps/knowbase/services/chat_service.py +239 -0
- django_cfg/apps/knowbase/services/document_service.py +144 -0
- django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
- django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
- django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
- django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
- django_cfg/apps/knowbase/services/embedding/models.py +229 -0
- django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
- django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
- django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
- django_cfg/apps/knowbase/services/search_service.py +293 -0
- django_cfg/apps/knowbase/signals/__init__.py +21 -0
- django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
- django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
- django_cfg/apps/knowbase/signals/document_signals.py +143 -0
- django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
- django_cfg/apps/knowbase/tasks/__init__.py +39 -0
- django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
- django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
- django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
- django_cfg/apps/knowbase/urls.py +43 -0
- django_cfg/apps/knowbase/utils/__init__.py +12 -0
- django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
- django_cfg/apps/knowbase/utils/text_processing.py +375 -0
- django_cfg/apps/knowbase/utils/validation.py +99 -0
- django_cfg/apps/knowbase/views/__init__.py +28 -0
- django_cfg/apps/knowbase/views/archive_views.py +469 -0
- django_cfg/apps/knowbase/views/base.py +49 -0
- django_cfg/apps/knowbase/views/chat_views.py +181 -0
- django_cfg/apps/knowbase/views/document_views.py +183 -0
- django_cfg/apps/knowbase/views/public_views.py +129 -0
- django_cfg/apps/leads/admin.py +70 -0
- django_cfg/apps/newsletter/admin.py +234 -0
- django_cfg/apps/newsletter/admin_filters.py +124 -0
- django_cfg/apps/support/admin.py +196 -0
- django_cfg/apps/support/admin_filters.py +71 -0
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +5 -4
- django_cfg/cli/README.md +1 -1
- django_cfg/cli/commands/create_project.py +2 -2
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/config.py +44 -0
- django_cfg/core/config.py +29 -82
- django_cfg/core/environment.py +1 -1
- django_cfg/core/generation.py +19 -107
- django_cfg/{integration.py โ core/integration.py} +18 -16
- django_cfg/core/validation.py +1 -1
- django_cfg/management/__init__.py +1 -1
- django_cfg/management/commands/__init__.py +1 -1
- django_cfg/management/commands/auto_generate.py +482 -0
- django_cfg/management/commands/migrator.py +19 -101
- django_cfg/management/commands/test_email.py +1 -1
- django_cfg/middleware/README.md +0 -158
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/api.py +145 -0
- django_cfg/models/base.py +287 -0
- django_cfg/models/cache.py +4 -4
- django_cfg/models/constance.py +25 -88
- django_cfg/models/database.py +9 -9
- django_cfg/models/drf.py +3 -36
- django_cfg/models/email.py +163 -0
- django_cfg/models/environment.py +276 -0
- django_cfg/models/limits.py +1 -1
- django_cfg/models/logging.py +366 -0
- django_cfg/models/revolution.py +41 -2
- django_cfg/models/security.py +125 -0
- django_cfg/models/services.py +1 -1
- django_cfg/modules/__init__.py +2 -56
- django_cfg/modules/base.py +78 -52
- django_cfg/modules/django_currency/service.py +2 -2
- django_cfg/modules/django_email.py +2 -2
- django_cfg/modules/django_health.py +267 -0
- django_cfg/modules/django_llm/llm/client.py +79 -17
- django_cfg/modules/django_llm/translator/translator.py +2 -2
- django_cfg/modules/django_logger.py +2 -2
- django_cfg/modules/django_ngrok.py +2 -2
- django_cfg/modules/django_tasks.py +68 -3
- django_cfg/modules/django_telegram.py +3 -3
- django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
- django_cfg/modules/django_twilio/service.py +2 -2
- django_cfg/modules/django_twilio/simple_service.py +2 -2
- django_cfg/modules/django_twilio/twilio_service.py +2 -2
- django_cfg/modules/django_unfold/__init__.py +69 -0
- django_cfg/modules/{unfold โ django_unfold}/callbacks.py +23 -22
- django_cfg/modules/django_unfold/dashboard.py +278 -0
- django_cfg/modules/django_unfold/icons/README.md +145 -0
- django_cfg/modules/django_unfold/icons/__init__.py +12 -0
- django_cfg/modules/django_unfold/icons/constants.py +2851 -0
- django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
- django_cfg/modules/django_unfold/models/__init__.py +42 -0
- django_cfg/modules/django_unfold/models/config.py +601 -0
- django_cfg/modules/django_unfold/models/dashboard.py +206 -0
- django_cfg/modules/django_unfold/models/dropdown.py +40 -0
- django_cfg/modules/django_unfold/models/navigation.py +73 -0
- django_cfg/modules/django_unfold/models/tabs.py +25 -0
- django_cfg/modules/{unfold โ django_unfold}/system_monitor.py +2 -2
- django_cfg/modules/django_unfold/utils.py +140 -0
- django_cfg/registry/__init__.py +23 -0
- django_cfg/registry/core.py +61 -0
- django_cfg/registry/exceptions.py +11 -0
- django_cfg/registry/modules.py +12 -0
- django_cfg/registry/services.py +26 -0
- django_cfg/registry/third_party.py +52 -0
- django_cfg/routing/__init__.py +19 -0
- django_cfg/routing/callbacks.py +198 -0
- django_cfg/routing/routers.py +48 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
- django_cfg/templatetags/__init__.py +0 -0
- django_cfg/templatetags/django_cfg.py +33 -0
- django_cfg/urls.py +33 -0
- django_cfg/utils/path_resolution.py +1 -1
- django_cfg/utils/smart_defaults.py +7 -61
- django_cfg/utils/toolkit.py +663 -0
- {django_cfg-1.1.82.dist-info โ django_cfg-1.2.0.dist-info}/METADATA +83 -86
- django_cfg-1.2.0.dist-info/RECORD +441 -0
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/models/unfold.py +0 -271
- django_cfg/modules/unfold/__init__.py +0 -29
- django_cfg/modules/unfold/dashboard.py +0 -318
- django_cfg/pyproject.toml +0 -370
- django_cfg/routers.py +0 -83
- django_cfg-1.1.82.dist-info/RECORD +0 -278
- /django_cfg/{exceptions.py โ core/exceptions.py} +0 -0
- /django_cfg/modules/{unfold โ django_unfold}/models.py +0 -0
- /django_cfg/modules/{unfold โ django_unfold}/tailwind.py +0 -0
- /django_cfg/{version_check.py โ utils/version_check.py} +0 -0
- {django_cfg-1.1.82.dist-info โ django_cfg-1.2.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.82.dist-info โ django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.82.dist-info โ django_cfg-1.2.0.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
|
+
]
|