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.
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 +444 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +79 -17
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/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.0.dist-info}/METADATA +83 -86
  230. django_cfg-1.2.0.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.0.dist-info}/WHEEL +0 -0
  243. {django_cfg-1.1.82.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
  244. {django_cfg-1.1.82.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)