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.
- 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/templates/guide.md +266 -0
- 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.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
- django_cfg-1.2.0.dist-info/RECORD +441 -0
- django_cfg/apps/tasks/@docs/README.md +0 -195
- 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.81.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.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,685 @@
|
|
1
|
+
"""
|
2
|
+
External Data admin interfaces with Unfold optimization.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.contrib import admin, messages
|
6
|
+
from django.utils.html import format_html
|
7
|
+
from django.urls import reverse
|
8
|
+
from django.utils.http import urlencode
|
9
|
+
from django.shortcuts import redirect
|
10
|
+
from django.db import models
|
11
|
+
from django.db.models import Count, Sum, Avg, Q
|
12
|
+
from django.db.models.fields.json import JSONField
|
13
|
+
from django.utils import timezone
|
14
|
+
from django import forms
|
15
|
+
from django_json_widget.widgets import JSONEditorWidget
|
16
|
+
from unfold.admin import ModelAdmin, TabularInline
|
17
|
+
from unfold.decorators import display, action
|
18
|
+
from unfold.enums import ActionVariant
|
19
|
+
from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
|
20
|
+
from unfold.contrib.forms.widgets import WysiwygWidget
|
21
|
+
|
22
|
+
from ..models.external_data import ExternalData, ExternalDataChunk, ExternalDataType, ExternalDataStatus
|
23
|
+
|
24
|
+
|
25
|
+
class ExternalDataChunkInline(TabularInline):
|
26
|
+
"""Inline for external data chunks with Unfold styling."""
|
27
|
+
|
28
|
+
model = ExternalDataChunk
|
29
|
+
verbose_name = "External Data Chunk"
|
30
|
+
verbose_name_plural = "🔗 External Data Chunks (Read-only)"
|
31
|
+
extra = 0
|
32
|
+
max_num = 0 # No new chunks allowed
|
33
|
+
can_delete = False # Prevent deletion through inline
|
34
|
+
show_change_link = True # Allow viewing individual chunks
|
35
|
+
|
36
|
+
def has_add_permission(self, request, obj=None):
|
37
|
+
"""Disable adding new chunks through inline."""
|
38
|
+
return False
|
39
|
+
|
40
|
+
def has_change_permission(self, request, obj=None):
|
41
|
+
"""Disable editing chunks through inline."""
|
42
|
+
return False
|
43
|
+
|
44
|
+
def has_delete_permission(self, request, obj=None):
|
45
|
+
"""Disable deleting chunks through inline."""
|
46
|
+
return False
|
47
|
+
|
48
|
+
fields = [
|
49
|
+
'short_uuid', 'chunk_index', 'content_preview_inline', 'token_count',
|
50
|
+
'has_embedding_inline', 'embedding_cost'
|
51
|
+
]
|
52
|
+
readonly_fields = [
|
53
|
+
'short_uuid', 'chunk_index', 'content_preview_inline', 'token_count', 'character_count',
|
54
|
+
'has_embedding_inline', 'embedding_cost', 'created_at'
|
55
|
+
]
|
56
|
+
|
57
|
+
# Unfold specific options
|
58
|
+
hide_title = False # Show titles for better UX
|
59
|
+
classes = ['collapse'] # Collapsed by default
|
60
|
+
|
61
|
+
@display(description="Content Preview")
|
62
|
+
def content_preview_inline(self, obj):
|
63
|
+
"""Shortened content preview for inline display."""
|
64
|
+
if not obj.content:
|
65
|
+
return "-"
|
66
|
+
preview = obj.content[:100] + "..." if len(obj.content) > 100 else obj.content
|
67
|
+
return format_html(
|
68
|
+
'<div style="max-width: 300px; font-size: 12px; color: #666;">{}</div>',
|
69
|
+
preview
|
70
|
+
)
|
71
|
+
|
72
|
+
@display(description="Has Embedding", boolean=True)
|
73
|
+
def has_embedding_inline(self, obj):
|
74
|
+
"""Check if chunk has embedding vector for inline."""
|
75
|
+
return obj.embedding is not None and len(obj.embedding) > 0
|
76
|
+
|
77
|
+
def get_queryset(self, request):
|
78
|
+
"""Optimize queryset for inline display."""
|
79
|
+
return super().get_queryset(request).select_related('external_data', 'user')
|
80
|
+
|
81
|
+
|
82
|
+
@admin.register(ExternalData)
|
83
|
+
class ExternalDataAdmin(ModelAdmin):
|
84
|
+
"""Admin interface for ExternalData model with Unfold styling."""
|
85
|
+
|
86
|
+
list_display = [
|
87
|
+
'title_display', 'source_type_badge', 'source_identifier_display', 'user',
|
88
|
+
'status_badge', 'chunks_count_display', 'tokens_display', 'cost_display',
|
89
|
+
'visibility_badge', 'processed_at', 'created_at'
|
90
|
+
]
|
91
|
+
ordering = ['-created_at'] # Newest first
|
92
|
+
inlines = [ExternalDataChunkInline]
|
93
|
+
list_filter = [
|
94
|
+
'source_type', 'status', 'is_active', 'is_public',
|
95
|
+
'embedding_model', 'processed_at', 'created_at',
|
96
|
+
('user', AutocompleteSelectFilter),
|
97
|
+
('category', AutocompleteSelectFilter)
|
98
|
+
]
|
99
|
+
search_fields = ['title', 'description', 'source_identifier', 'user__username', 'user__email']
|
100
|
+
autocomplete_fields = ['user', 'category']
|
101
|
+
readonly_fields = [
|
102
|
+
'id', 'user', 'status', 'total_chunks', 'total_tokens', 'processing_cost',
|
103
|
+
'processed_at', 'source_updated_at', 'processing_error', 'content_hash',
|
104
|
+
'created_at', 'updated_at'
|
105
|
+
]
|
106
|
+
|
107
|
+
fieldsets = (
|
108
|
+
('📝 Basic Information', {
|
109
|
+
'fields': ('title', 'description', 'category'),
|
110
|
+
'description': 'Main information about the external data source'
|
111
|
+
}),
|
112
|
+
('📄 Content', {
|
113
|
+
'fields': ('content',),
|
114
|
+
'description': 'Main content for vectorization'
|
115
|
+
}),
|
116
|
+
('🔗 Source Configuration', {
|
117
|
+
'fields': ('source_type', 'source_identifier', 'source_config'),
|
118
|
+
'description': 'Configure how data is extracted from the source'
|
119
|
+
}),
|
120
|
+
('⚙️ Processing Settings', {
|
121
|
+
'fields': ('chunk_size', 'overlap_size', 'embedding_model', 'similarity_threshold'),
|
122
|
+
'description': 'Settings for text chunking, vectorization, and search'
|
123
|
+
}),
|
124
|
+
('🔒 Access Control', {
|
125
|
+
'fields': ('is_active', 'is_public'),
|
126
|
+
'description': 'Control visibility and access to this data'
|
127
|
+
}),
|
128
|
+
('📊 Processing Status (Read-only)', {
|
129
|
+
'fields': ('status', 'processing_error', 'processed_at', 'source_updated_at', 'content_hash'),
|
130
|
+
'classes': ('collapse',),
|
131
|
+
'description': 'Current processing status and error information'
|
132
|
+
}),
|
133
|
+
('📈 Statistics (Read-only)', {
|
134
|
+
'fields': ('total_chunks', 'total_tokens', 'processing_cost'),
|
135
|
+
'classes': ('collapse',),
|
136
|
+
'description': 'Processing statistics and costs'
|
137
|
+
}),
|
138
|
+
('📋 Additional Data (Read-only)', {
|
139
|
+
'fields': ('metadata', 'tags'),
|
140
|
+
'classes': ('collapse',),
|
141
|
+
'description': 'Auto-generated metadata and tags'
|
142
|
+
}),
|
143
|
+
('🔧 System Information (Read-only)', {
|
144
|
+
'fields': ('id', 'user', 'created_at', 'updated_at'),
|
145
|
+
'classes': ('collapse',),
|
146
|
+
'description': 'System-generated information'
|
147
|
+
})
|
148
|
+
)
|
149
|
+
|
150
|
+
# Unfold configuration
|
151
|
+
compressed_fields = True
|
152
|
+
warn_unsaved_form = True
|
153
|
+
|
154
|
+
# Form field overrides
|
155
|
+
formfield_overrides = {
|
156
|
+
JSONField: {
|
157
|
+
"widget": JSONEditorWidget,
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
162
|
+
"""Custom field handling for better UX."""
|
163
|
+
if db_field.name == 'content':
|
164
|
+
kwargs['widget'] = WysiwygWidget()
|
165
|
+
elif db_field.name == 'description':
|
166
|
+
kwargs['widget'] = forms.Textarea(attrs={
|
167
|
+
'rows': 3,
|
168
|
+
'cols': 80,
|
169
|
+
'style': 'width: 100%;'
|
170
|
+
})
|
171
|
+
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
172
|
+
|
173
|
+
actions = [
|
174
|
+
'mark_for_reprocessing',
|
175
|
+
'regenerate_embeddings',
|
176
|
+
'activate_selected',
|
177
|
+
'deactivate_selected',
|
178
|
+
'make_public',
|
179
|
+
'make_private'
|
180
|
+
]
|
181
|
+
|
182
|
+
# Actions for detail view (individual object edit page)
|
183
|
+
actions_detail = ["reprocess_external_data"]
|
184
|
+
|
185
|
+
def get_queryset(self, request):
|
186
|
+
"""Optimize queryset with select_related and prefetch_related."""
|
187
|
+
queryset = super().get_queryset(request).select_related('user', 'category')
|
188
|
+
queryset = queryset.annotate(
|
189
|
+
annotated_chunks_count=Count('chunks'),
|
190
|
+
total_embedding_cost=Sum('chunks__embedding_cost')
|
191
|
+
)
|
192
|
+
|
193
|
+
# For non-superusers, filter by their own external data
|
194
|
+
if not request.user.is_superuser:
|
195
|
+
queryset = queryset.filter(user=request.user)
|
196
|
+
|
197
|
+
return queryset
|
198
|
+
|
199
|
+
def save_model(self, request, obj, form, change):
|
200
|
+
"""Automatically set user to current user when creating new external data."""
|
201
|
+
if not change: # Only for new external data
|
202
|
+
obj.user = request.user
|
203
|
+
super().save_model(request, obj, form, change)
|
204
|
+
|
205
|
+
@display(description="External Data Title", ordering="title")
|
206
|
+
def title_display(self, obj):
|
207
|
+
"""Display external data title with truncation."""
|
208
|
+
title = obj.title or "Untitled External Data"
|
209
|
+
if len(title) > 50:
|
210
|
+
title = title[:47] + "..."
|
211
|
+
return format_html(
|
212
|
+
'<div style="font-weight: 500;">{}</div>',
|
213
|
+
title
|
214
|
+
)
|
215
|
+
|
216
|
+
@display(
|
217
|
+
description="Source Type",
|
218
|
+
ordering="source_type",
|
219
|
+
label={
|
220
|
+
'model': 'info', # blue for Django models
|
221
|
+
'api': 'success', # green for API
|
222
|
+
'database': 'warning', # orange for database
|
223
|
+
'file': 'secondary', # gray for files
|
224
|
+
'custom': 'danger' # red for custom
|
225
|
+
}
|
226
|
+
)
|
227
|
+
def source_type_badge(self, obj):
|
228
|
+
"""Display source type with color coding."""
|
229
|
+
return obj.source_type, obj.get_source_type_display()
|
230
|
+
|
231
|
+
@display(description="Source Identifier", ordering="source_identifier")
|
232
|
+
def source_identifier_display(self, obj):
|
233
|
+
"""Display source identifier with truncation."""
|
234
|
+
identifier = obj.source_identifier
|
235
|
+
if len(identifier) > 30:
|
236
|
+
identifier = identifier[:27] + "..."
|
237
|
+
return format_html(
|
238
|
+
'<code style="font-size: 12px;">{}</code>',
|
239
|
+
identifier
|
240
|
+
)
|
241
|
+
|
242
|
+
@display(
|
243
|
+
description="Status",
|
244
|
+
ordering="status",
|
245
|
+
label={
|
246
|
+
'pending': 'warning', # orange for pending
|
247
|
+
'processing': 'info', # blue for processing
|
248
|
+
'completed': 'success', # green for completed
|
249
|
+
'failed': 'danger', # red for failed
|
250
|
+
'outdated': 'secondary' # gray for outdated
|
251
|
+
}
|
252
|
+
)
|
253
|
+
def status_badge(self, obj):
|
254
|
+
"""Display processing status with color coding."""
|
255
|
+
return obj.status, obj.get_status_display()
|
256
|
+
|
257
|
+
@display(
|
258
|
+
description="Visibility",
|
259
|
+
ordering="is_active",
|
260
|
+
label={
|
261
|
+
True: 'success', # green for active
|
262
|
+
False: 'danger' # red for inactive
|
263
|
+
}
|
264
|
+
)
|
265
|
+
def visibility_badge(self, obj):
|
266
|
+
"""Display visibility status with color coding."""
|
267
|
+
if obj.is_active:
|
268
|
+
status = "Active" + (" & Public" if obj.is_public else " & Private")
|
269
|
+
return True, status
|
270
|
+
else:
|
271
|
+
return False, "Inactive"
|
272
|
+
|
273
|
+
@display(description="Chunks", ordering="annotated_chunks_count")
|
274
|
+
def chunks_count_display(self, obj):
|
275
|
+
"""Display chunks count with link."""
|
276
|
+
count = getattr(obj, 'annotated_chunks_count', obj.chunks.count())
|
277
|
+
if count > 0:
|
278
|
+
url = f"/admin/knowbase/externaldatachunk/?external_data__id__exact={obj.id}"
|
279
|
+
return format_html(
|
280
|
+
'<a href="{}" style="text-decoration: none;">{} chunks</a>',
|
281
|
+
url, count
|
282
|
+
)
|
283
|
+
return "0 chunks"
|
284
|
+
|
285
|
+
@display(description="Tokens", ordering="total_tokens")
|
286
|
+
def tokens_display(self, obj):
|
287
|
+
"""Display token count with formatting."""
|
288
|
+
tokens = obj.total_tokens
|
289
|
+
if tokens > 1000:
|
290
|
+
return f"{tokens/1000:.1f}K"
|
291
|
+
return str(tokens)
|
292
|
+
|
293
|
+
@display(description="Cost (USD)", ordering="processing_cost")
|
294
|
+
def cost_display(self, obj):
|
295
|
+
"""Display cost with currency formatting."""
|
296
|
+
return f"${obj.processing_cost:.6f}"
|
297
|
+
|
298
|
+
@display(description="Content Hash")
|
299
|
+
def content_hash_display(self, obj):
|
300
|
+
"""Display content hash for change tracking."""
|
301
|
+
if obj.content_hash:
|
302
|
+
return format_html(
|
303
|
+
'<code style="font-size: 10px; color: #666;">{}</code>',
|
304
|
+
obj.content_hash[:12] + "..."
|
305
|
+
)
|
306
|
+
return "-"
|
307
|
+
|
308
|
+
def changelist_view(self, request, extra_context=None):
|
309
|
+
"""Add summary statistics to changelist."""
|
310
|
+
extra_context = extra_context or {}
|
311
|
+
|
312
|
+
# Get summary statistics
|
313
|
+
queryset = self.get_queryset(request)
|
314
|
+
stats = queryset.aggregate(
|
315
|
+
total_external_data=Count('id'),
|
316
|
+
active_external_data=Count('id', filter=Q(is_active=True)),
|
317
|
+
total_chunks=Sum('total_chunks'),
|
318
|
+
total_tokens=Sum('total_tokens'),
|
319
|
+
total_cost=Sum('processing_cost')
|
320
|
+
)
|
321
|
+
|
322
|
+
# Status breakdown
|
323
|
+
status_counts = dict(
|
324
|
+
queryset.values_list('status').annotate(
|
325
|
+
count=Count('id')
|
326
|
+
)
|
327
|
+
)
|
328
|
+
|
329
|
+
# Source type breakdown
|
330
|
+
source_type_counts = dict(
|
331
|
+
queryset.values_list('source_type').annotate(
|
332
|
+
count=Count('id')
|
333
|
+
)
|
334
|
+
)
|
335
|
+
|
336
|
+
extra_context['external_data_stats'] = {
|
337
|
+
'total_external_data': stats['total_external_data'] or 0,
|
338
|
+
'active_external_data': stats['active_external_data'] or 0,
|
339
|
+
'total_chunks': stats['total_chunks'] or 0,
|
340
|
+
'total_tokens': stats['total_tokens'] or 0,
|
341
|
+
'total_cost': f"${(stats['total_cost'] or 0):.6f}",
|
342
|
+
'status_counts': status_counts,
|
343
|
+
'source_type_counts': source_type_counts
|
344
|
+
}
|
345
|
+
|
346
|
+
return super().changelist_view(request, extra_context)
|
347
|
+
|
348
|
+
@admin.action(description='Mark selected for reprocessing')
|
349
|
+
def mark_for_reprocessing(self, request, queryset):
|
350
|
+
"""Mark external data for reprocessing."""
|
351
|
+
updated = queryset.update(
|
352
|
+
status=ExternalDataStatus.PENDING,
|
353
|
+
processing_error='',
|
354
|
+
updated_at=timezone.now()
|
355
|
+
)
|
356
|
+
self.message_user(
|
357
|
+
request,
|
358
|
+
f"Marked {updated} external data sources for reprocessing."
|
359
|
+
)
|
360
|
+
|
361
|
+
@admin.action(description='Activate selected external data')
|
362
|
+
def activate_selected(self, request, queryset):
|
363
|
+
"""Activate selected external data."""
|
364
|
+
updated = queryset.update(is_active=True)
|
365
|
+
self.message_user(
|
366
|
+
request,
|
367
|
+
f"Activated {updated} external data sources."
|
368
|
+
)
|
369
|
+
|
370
|
+
@admin.action(description='Deactivate selected external data')
|
371
|
+
def deactivate_selected(self, request, queryset):
|
372
|
+
"""Deactivate selected external data."""
|
373
|
+
updated = queryset.update(is_active=False)
|
374
|
+
self.message_user(
|
375
|
+
request,
|
376
|
+
f"Deactivated {updated} external data sources."
|
377
|
+
)
|
378
|
+
|
379
|
+
@admin.action(description='Make selected external data public')
|
380
|
+
def make_public(self, request, queryset):
|
381
|
+
"""Make selected external data public."""
|
382
|
+
updated = queryset.update(is_public=True)
|
383
|
+
self.message_user(
|
384
|
+
request,
|
385
|
+
f"Made {updated} external data sources public."
|
386
|
+
)
|
387
|
+
|
388
|
+
@admin.action(description='Make selected external data private')
|
389
|
+
def make_private(self, request, queryset):
|
390
|
+
"""Make selected external data private."""
|
391
|
+
updated = queryset.update(is_public=False)
|
392
|
+
self.message_user(
|
393
|
+
request,
|
394
|
+
f"Made {updated} external data sources private."
|
395
|
+
)
|
396
|
+
|
397
|
+
@admin.action(description='🔄 Regenerate embeddings for selected external data')
|
398
|
+
def regenerate_embeddings(self, request, queryset):
|
399
|
+
"""Regenerate embeddings for selected external data sources."""
|
400
|
+
external_data_ids = [str(obj.id) for obj in queryset]
|
401
|
+
|
402
|
+
if not external_data_ids:
|
403
|
+
self.message_user(
|
404
|
+
request,
|
405
|
+
"No external data selected for regeneration.",
|
406
|
+
level=messages.WARNING
|
407
|
+
)
|
408
|
+
return
|
409
|
+
|
410
|
+
try:
|
411
|
+
# Use manager method for regeneration
|
412
|
+
result = ExternalData.objects.regenerate_external_data(external_data_ids)
|
413
|
+
|
414
|
+
if result['success']:
|
415
|
+
success_msg = (
|
416
|
+
f"✅ Successfully queued {result['regenerated_count']} external data sources "
|
417
|
+
f"for regeneration."
|
418
|
+
)
|
419
|
+
|
420
|
+
if result['failed_count'] > 0:
|
421
|
+
success_msg += f" {result['failed_count']} failed to queue."
|
422
|
+
|
423
|
+
self.message_user(request, success_msg, level=messages.SUCCESS)
|
424
|
+
|
425
|
+
# Show errors if any
|
426
|
+
if result.get('errors'):
|
427
|
+
for error in result['errors'][:3]: # Show first 3 errors
|
428
|
+
self.message_user(request, f"❌ {error}", level=messages.ERROR)
|
429
|
+
|
430
|
+
if len(result['errors']) > 3:
|
431
|
+
self.message_user(
|
432
|
+
request,
|
433
|
+
f"... and {len(result['errors']) - 3} more errors.",
|
434
|
+
level=messages.WARNING
|
435
|
+
)
|
436
|
+
else:
|
437
|
+
error_msg = result.get('error', 'Unknown error occurred')
|
438
|
+
self.message_user(
|
439
|
+
request,
|
440
|
+
f"❌ Failed to regenerate external data: {error_msg}",
|
441
|
+
level=messages.ERROR
|
442
|
+
)
|
443
|
+
|
444
|
+
except Exception as e:
|
445
|
+
self.message_user(
|
446
|
+
request,
|
447
|
+
f"❌ Error during regeneration: {str(e)}",
|
448
|
+
level=messages.ERROR
|
449
|
+
)
|
450
|
+
|
451
|
+
@action(
|
452
|
+
description="🔄 Reprocess External Data",
|
453
|
+
icon="refresh",
|
454
|
+
variant=ActionVariant.WARNING
|
455
|
+
)
|
456
|
+
def reprocess_external_data(self, request, object_id):
|
457
|
+
"""Force reprocessing of the external data source."""
|
458
|
+
try:
|
459
|
+
# Get the external data object
|
460
|
+
external_data = self.get_object(request, object_id)
|
461
|
+
if not external_data:
|
462
|
+
self.message_user(
|
463
|
+
request,
|
464
|
+
"External data not found.",
|
465
|
+
level=messages.ERROR
|
466
|
+
)
|
467
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
468
|
+
|
469
|
+
# Use manager method for regeneration
|
470
|
+
result = ExternalData.objects.regenerate_external_data([str(external_data.id)])
|
471
|
+
|
472
|
+
if result['success']:
|
473
|
+
self.message_user(
|
474
|
+
request,
|
475
|
+
f"✅ Successfully queued '{external_data.title}' for reprocessing. "
|
476
|
+
f"Processing will begin shortly in the background.",
|
477
|
+
level=messages.SUCCESS
|
478
|
+
)
|
479
|
+
else:
|
480
|
+
error_msg = result.get('error', 'Unknown error occurred')
|
481
|
+
self.message_user(
|
482
|
+
request,
|
483
|
+
f"❌ Failed to reprocess '{external_data.title}': {error_msg}",
|
484
|
+
level=messages.ERROR
|
485
|
+
)
|
486
|
+
|
487
|
+
except Exception as e:
|
488
|
+
self.message_user(
|
489
|
+
request,
|
490
|
+
f"❌ Error during reprocessing: {str(e)}",
|
491
|
+
level=messages.ERROR
|
492
|
+
)
|
493
|
+
|
494
|
+
# Redirect back to the same page
|
495
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
496
|
+
|
497
|
+
def has_add_permission(self, request):
|
498
|
+
"""Allow adding external data."""
|
499
|
+
return True
|
500
|
+
|
501
|
+
def has_change_permission(self, request, obj=None):
|
502
|
+
"""Check change permissions."""
|
503
|
+
if obj is not None and not request.user.is_superuser:
|
504
|
+
return obj.user == request.user
|
505
|
+
return super().has_change_permission(request, obj)
|
506
|
+
|
507
|
+
def has_delete_permission(self, request, obj=None):
|
508
|
+
"""Check delete permissions."""
|
509
|
+
if obj is not None and not request.user.is_superuser:
|
510
|
+
return obj.user == request.user
|
511
|
+
return super().has_delete_permission(request, obj)
|
512
|
+
|
513
|
+
|
514
|
+
@admin.register(ExternalDataChunk)
|
515
|
+
class ExternalDataChunkAdmin(ModelAdmin):
|
516
|
+
"""Admin interface for ExternalDataChunk model with Unfold styling."""
|
517
|
+
|
518
|
+
list_display = [
|
519
|
+
'chunk_display', 'external_data_link', 'user', 'token_count_display',
|
520
|
+
'embedding_status', 'embedding_cost_display', 'created_at'
|
521
|
+
]
|
522
|
+
ordering = ['-created_at'] # Newest first
|
523
|
+
list_filter = [
|
524
|
+
'embedding_model', 'external_data__source_type',
|
525
|
+
'external_data__status', 'created_at',
|
526
|
+
('user', AutocompleteSelectFilter),
|
527
|
+
('external_data', AutocompleteSelectFilter)
|
528
|
+
]
|
529
|
+
search_fields = ['external_data__title', 'external_data__source_identifier', 'content', 'user__username']
|
530
|
+
readonly_fields = [
|
531
|
+
'id', 'user', 'external_data', 'chunk_index', 'embedding_info',
|
532
|
+
'token_count', 'character_count', 'embedding_cost', 'embedding_model',
|
533
|
+
'created_at', 'updated_at', 'content_preview'
|
534
|
+
]
|
535
|
+
|
536
|
+
fieldsets = (
|
537
|
+
('📄 Content', {
|
538
|
+
'fields': ('content_preview', 'content'),
|
539
|
+
'description': 'Text content of this chunk'
|
540
|
+
}),
|
541
|
+
('🔗 Relationships (Read-only)', {
|
542
|
+
'fields': ('external_data', 'user', 'chunk_index'),
|
543
|
+
'description': 'Links to parent external data and owner'
|
544
|
+
}),
|
545
|
+
('📊 Metrics (Read-only)', {
|
546
|
+
'fields': ('embedding_info', 'token_count', 'character_count', 'embedding_cost'),
|
547
|
+
'description': 'Processing metrics and costs'
|
548
|
+
}),
|
549
|
+
('🧠 Vector Embedding (Read-only)', {
|
550
|
+
'fields': ('embedding_model', 'embedding'),
|
551
|
+
'classes': ('collapse',),
|
552
|
+
'description': 'Vector representation for semantic search'
|
553
|
+
}),
|
554
|
+
('📋 Metadata (Read-only)', {
|
555
|
+
'fields': ('chunk_metadata',),
|
556
|
+
'classes': ('collapse',),
|
557
|
+
'description': 'Auto-generated chunk metadata'
|
558
|
+
}),
|
559
|
+
('🔧 System Information (Read-only)', {
|
560
|
+
'fields': ('id', 'created_at', 'updated_at'),
|
561
|
+
'classes': ('collapse',),
|
562
|
+
'description': 'System-generated information'
|
563
|
+
})
|
564
|
+
)
|
565
|
+
|
566
|
+
# Unfold configuration
|
567
|
+
compressed_fields = True
|
568
|
+
warn_unsaved_form = True
|
569
|
+
|
570
|
+
# Form field overrides
|
571
|
+
formfield_overrides = {
|
572
|
+
JSONField: {
|
573
|
+
"widget": JSONEditorWidget,
|
574
|
+
}
|
575
|
+
}
|
576
|
+
|
577
|
+
def get_queryset(self, request):
|
578
|
+
"""Optimize queryset with select_related."""
|
579
|
+
return super().get_queryset(request).select_related('external_data', 'user')
|
580
|
+
|
581
|
+
@display(description="Chunk", ordering="chunk_index")
|
582
|
+
def chunk_display(self, obj):
|
583
|
+
"""Display chunk identifier."""
|
584
|
+
return f"Chunk {obj.chunk_index + 1}"
|
585
|
+
|
586
|
+
@display(description="Tokens", ordering="token_count")
|
587
|
+
def token_count_display(self, obj):
|
588
|
+
"""Display token count with formatting."""
|
589
|
+
tokens = obj.token_count
|
590
|
+
if tokens > 1000:
|
591
|
+
return f"{tokens/1000:.1f}K"
|
592
|
+
return str(tokens)
|
593
|
+
|
594
|
+
@display(
|
595
|
+
description="Embedding",
|
596
|
+
label={
|
597
|
+
True: 'success', # green for has embedding
|
598
|
+
False: 'danger' # red for no embedding
|
599
|
+
}
|
600
|
+
)
|
601
|
+
def embedding_status(self, obj):
|
602
|
+
"""Display embedding status."""
|
603
|
+
has_embedding = obj.embedding is not None and len(obj.embedding) > 0
|
604
|
+
return has_embedding, "✓ Vectorized" if has_embedding else "✗ Not vectorized"
|
605
|
+
|
606
|
+
@display(description="External Data", ordering="external_data__title")
|
607
|
+
def external_data_link(self, obj):
|
608
|
+
"""Display external data title with admin link."""
|
609
|
+
url = reverse('admin:django_cfg_knowbase_externaldata_change', args=[obj.external_data.id])
|
610
|
+
return format_html(
|
611
|
+
'<a href="{}" style="text-decoration: none;">{}</a>',
|
612
|
+
url,
|
613
|
+
obj.external_data.title
|
614
|
+
)
|
615
|
+
|
616
|
+
@display(description="Cost (USD)", ordering="embedding_cost")
|
617
|
+
def embedding_cost_display(self, obj):
|
618
|
+
"""Display embedding cost with currency formatting."""
|
619
|
+
return f"${obj.embedding_cost:.6f}"
|
620
|
+
|
621
|
+
@display(description="Content Preview")
|
622
|
+
def content_preview(self, obj):
|
623
|
+
"""Display content preview with truncation."""
|
624
|
+
preview = obj.content[:200] + "..." if len(obj.content) > 200 else obj.content
|
625
|
+
return format_html(
|
626
|
+
'<div style="max-width: 400px; word-wrap: break-word;">{}</div>',
|
627
|
+
preview
|
628
|
+
)
|
629
|
+
|
630
|
+
@display(description="Embedding Info")
|
631
|
+
def embedding_info(self, obj):
|
632
|
+
"""Display embedding information safely."""
|
633
|
+
if obj.embedding is not None and len(obj.embedding) > 0:
|
634
|
+
return format_html(
|
635
|
+
'<span style="color: green;">✓ Vector ({} dimensions)</span>',
|
636
|
+
len(obj.embedding)
|
637
|
+
)
|
638
|
+
return format_html(
|
639
|
+
'<span style="color: red;">✗ No embedding</span>'
|
640
|
+
)
|
641
|
+
|
642
|
+
def changelist_view(self, request, extra_context=None):
|
643
|
+
"""Add chunk statistics to changelist."""
|
644
|
+
extra_context = extra_context or {}
|
645
|
+
|
646
|
+
queryset = self.get_queryset(request)
|
647
|
+
stats = queryset.aggregate(
|
648
|
+
total_chunks=Count('id'),
|
649
|
+
total_tokens=Sum('token_count'),
|
650
|
+
total_characters=Sum('character_count'),
|
651
|
+
total_embedding_cost=Sum('embedding_cost'),
|
652
|
+
avg_tokens_per_chunk=Avg('token_count')
|
653
|
+
)
|
654
|
+
|
655
|
+
# Model breakdown
|
656
|
+
model_counts = dict(
|
657
|
+
queryset.values_list('embedding_model').annotate(
|
658
|
+
count=Count('id')
|
659
|
+
)
|
660
|
+
)
|
661
|
+
|
662
|
+
extra_context['chunk_stats'] = {
|
663
|
+
'total_chunks': stats['total_chunks'] or 0,
|
664
|
+
'total_tokens': stats['total_tokens'] or 0,
|
665
|
+
'total_characters': stats['total_characters'] or 0,
|
666
|
+
'total_embedding_cost': f"${(stats['total_embedding_cost'] or 0):.6f}",
|
667
|
+
'avg_tokens_per_chunk': f"{(stats['avg_tokens_per_chunk'] or 0):.0f}",
|
668
|
+
'model_counts': model_counts
|
669
|
+
}
|
670
|
+
|
671
|
+
return super().changelist_view(request, extra_context)
|
672
|
+
|
673
|
+
def has_add_permission(self, request):
|
674
|
+
"""Chunks are created automatically."""
|
675
|
+
return False
|
676
|
+
|
677
|
+
def has_change_permission(self, request, obj=None):
|
678
|
+
"""Chunks are read-only."""
|
679
|
+
return False
|
680
|
+
|
681
|
+
def has_delete_permission(self, request, obj=None):
|
682
|
+
"""Allow deletion for cleanup."""
|
683
|
+
if obj is not None and not request.user.is_superuser:
|
684
|
+
return obj.user == request.user
|
685
|
+
return super().has_delete_permission(request, obj)
|