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,32 @@
1
+ """
2
+ Base managers for knowbase models.
3
+ """
4
+
5
+ from django.db import models
6
+ from django.contrib.auth import get_user_model
7
+
8
+ User = get_user_model()
9
+
10
+
11
+ class BaseKnowbaseManager(models.Manager):
12
+ """Base manager with common functionality for knowbase models."""
13
+
14
+ def for_user(self, user):
15
+ """Explicitly filter by specific user."""
16
+ return self.get_queryset().filter(user=user)
17
+
18
+ def all_users(self):
19
+ """Get unfiltered queryset (admin use)."""
20
+ return self.get_queryset()
21
+
22
+ def public(self):
23
+ """Get public records (if model has is_public field)."""
24
+ if hasattr(self.model, 'is_public'):
25
+ return self.get_queryset().filter(is_public=True)
26
+ return self.get_queryset()
27
+
28
+ def active(self):
29
+ """Get active records (if model has is_active field)."""
30
+ if hasattr(self.model, 'is_active'):
31
+ return self.get_queryset().filter(is_active=True)
32
+ return self.get_queryset()
@@ -0,0 +1,141 @@
1
+ """
2
+ Chat managers for session and message management.
3
+ """
4
+
5
+ from django.db import models
6
+ from django.db.models import Count, Sum, Avg, Q
7
+ from typing import Optional, List
8
+ from django.contrib.auth import get_user_model
9
+
10
+ User = get_user_model()
11
+
12
+
13
+ class ChatSessionManager(models.Manager):
14
+ """Custom manager for ChatSession model."""
15
+
16
+ def for_user(self, user):
17
+ """Explicitly filter by specific user."""
18
+ return self.get_queryset().filter(user=user)
19
+
20
+ def all_users(self):
21
+ """Get unfiltered queryset (admin use)."""
22
+ return self.get_queryset()
23
+
24
+ def active(self):
25
+ """Get only active chat sessions."""
26
+ return self.get_queryset().filter(is_active=True)
27
+
28
+ def inactive(self):
29
+ """Get only inactive chat sessions."""
30
+ return self.get_queryset().filter(is_active=False)
31
+
32
+ def recent(self, limit: int = 10):
33
+ """Get recent sessions ordered by update time."""
34
+ return self.get_queryset().order_by('-updated_at')[:limit]
35
+
36
+ def by_model(self, model_name: str):
37
+ """Get sessions using specific model."""
38
+ return self.get_queryset().filter(model_name=model_name)
39
+
40
+ def with_message_count(self):
41
+ """Get sessions with message count annotation."""
42
+ return self.get_queryset().annotate(
43
+ message_count=Count('messages')
44
+ )
45
+
46
+ def with_stats(self):
47
+ """Get sessions with full statistics."""
48
+ return self.get_queryset().select_related('user').prefetch_related('messages')
49
+
50
+ def search_by_title(self, query: str):
51
+ """Search sessions by title."""
52
+ return self.get_queryset().filter(
53
+ title__icontains=query
54
+ ).order_by('-updated_at')
55
+
56
+ def get_user_statistics(self, user=None):
57
+ """Get user's chat statistics."""
58
+ queryset = self.for_user(user) if user else self.get_queryset()
59
+
60
+ return queryset.aggregate(
61
+ total_sessions=Count('id'),
62
+ active_sessions=Count('id', filter=Q(is_active=True)),
63
+ total_messages=Sum('messages_count'),
64
+ total_tokens=Sum('total_tokens_used'),
65
+ total_cost=Sum('total_cost_usd'),
66
+ avg_messages_per_session=Avg('messages_count'),
67
+ avg_cost_per_session=Avg('total_cost_usd')
68
+ )
69
+
70
+
71
+ class ChatMessageManager(models.Manager):
72
+ """Custom manager for ChatMessage model."""
73
+
74
+ def for_user(self, user):
75
+ """Explicitly filter by specific user."""
76
+ return self.get_queryset().filter(user=user)
77
+
78
+ def all_users(self):
79
+ """Get unfiltered queryset (admin use)."""
80
+ return self.get_queryset()
81
+
82
+ def for_session(self, session_id: str):
83
+ """Get messages for specific session."""
84
+ return self.get_queryset().filter(session_id=session_id).order_by('created_at')
85
+
86
+ def user_messages(self):
87
+ """Get only user messages."""
88
+ return self.get_queryset().filter(role='user')
89
+
90
+ def assistant_messages(self):
91
+ """Get only assistant messages."""
92
+ return self.get_queryset().filter(role='assistant')
93
+
94
+ def system_messages(self):
95
+ """Get only system messages."""
96
+ return self.get_queryset().filter(role='system')
97
+
98
+ def by_model(self, model_name: str):
99
+ """Get messages generated by specific model."""
100
+ return self.get_queryset().filter(model_name=model_name)
101
+
102
+ def recent(self, limit: int = 50):
103
+ """Get recent messages."""
104
+ return self.get_queryset().order_by('-created_at')[:limit]
105
+
106
+ def with_context(self):
107
+ """Get messages that have context chunks."""
108
+ return self.get_queryset().exclude(context_chunks=[])
109
+
110
+ def expensive_messages(self, min_cost: float = 0.01):
111
+ """Get messages above certain cost threshold."""
112
+ return self.get_queryset().filter(cost_usd__gte=min_cost)
113
+
114
+ def slow_messages(self, min_time_ms: int = 5000):
115
+ """Get messages that took long to process."""
116
+ return self.get_queryset().filter(processing_time_ms__gte=min_time_ms)
117
+
118
+ def get_conversation_history(self, session_id: str, limit: Optional[int] = None):
119
+ """Get conversation history for session."""
120
+ queryset = self.for_session(session_id)
121
+ if limit:
122
+ # Get last N messages
123
+ queryset = queryset.order_by('-created_at')[:limit]
124
+ # Reverse to get chronological order
125
+ queryset = reversed(queryset)
126
+ return list(queryset)
127
+
128
+ def get_usage_statistics(self, user=None):
129
+ """Get message usage statistics."""
130
+ queryset = self.for_user(user) if user else self.get_queryset()
131
+
132
+ return queryset.aggregate(
133
+ total_messages=Count('id'),
134
+ user_messages=Count('id', filter=Q(role='user')),
135
+ assistant_messages=Count('id', filter=Q(role='assistant')),
136
+ total_tokens=Sum('tokens_used'),
137
+ total_cost=Sum('cost_usd'),
138
+ avg_tokens_per_message=Avg('tokens_used'),
139
+ avg_cost_per_message=Avg('cost_usd'),
140
+ avg_processing_time=Avg('processing_time_ms')
141
+ )
@@ -0,0 +1,203 @@
1
+ """
2
+ Document and DocumentChunk managers.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Dict, Any
7
+ from django.db import models
8
+ from django.db.models import Count, Q
9
+ from django.contrib.auth import get_user_model
10
+
11
+ logger = logging.getLogger(__name__)
12
+ User = get_user_model()
13
+
14
+
15
+ class DocumentManager(models.Manager):
16
+ """Custom manager for Document model."""
17
+
18
+ def for_user(self, user):
19
+ """Explicitly filter by specific user."""
20
+ return self.get_queryset().filter(user=user)
21
+
22
+ def all_users(self):
23
+ """Get unfiltered queryset (admin use)."""
24
+ return self.get_queryset()
25
+
26
+ def processed(self):
27
+ """Get only processed documents."""
28
+ from ..models.base import ProcessingStatus
29
+ return self.get_queryset().filter(
30
+ processing_status=ProcessingStatus.COMPLETED
31
+ )
32
+
33
+ def pending_processing(self):
34
+ """Get documents pending processing."""
35
+ from ..models.base import ProcessingStatus
36
+ return self.get_queryset().filter(
37
+ processing_status=ProcessingStatus.PENDING
38
+ )
39
+
40
+ def by_content_hash(self, content_hash: str):
41
+ """Find documents by content hash."""
42
+ return self.get_queryset().filter(content_hash=content_hash)
43
+
44
+ def with_stats(self):
45
+ """Get documents with chunk statistics."""
46
+ return self.get_queryset().select_related().prefetch_related('chunks')
47
+
48
+ def get_vectorization_progress(self, document_id):
49
+ """Get vectorization progress for a document."""
50
+ try:
51
+ from ..models.document import DocumentChunk
52
+
53
+ # Get all chunks for this document
54
+ chunks_qs = DocumentChunk.objects.filter(document_id=document_id)
55
+
56
+ total = chunks_qs.count()
57
+
58
+ # Count vectorized chunks by checking if embedding has non-zero values
59
+ vectorized = 0
60
+ if total > 0:
61
+ for chunk in chunks_qs.only('embedding'):
62
+ if chunk.embedding is not None and len(chunk.embedding) > 0 and any(x != 0.0 for x in chunk.embedding):
63
+ vectorized += 1
64
+
65
+ return {
66
+ 'total': total,
67
+ 'vectorized': vectorized,
68
+ 'percentage': round((vectorized / total * 100) if total > 0 else 0, 1)
69
+ }
70
+ except Exception as e:
71
+ logger.error(f"Error getting vectorization progress for document {document_id}: {e}")
72
+ return {
73
+ 'total': 0,
74
+ 'vectorized': 0,
75
+ 'percentage': 0
76
+ }
77
+
78
+ def get_vectorization_status_display(self, document):
79
+ """Get vectorization status display for admin."""
80
+ try:
81
+ # Check processing status first
82
+ from ..models.document import ProcessingStatus
83
+
84
+ if document.processing_status == ProcessingStatus.PENDING:
85
+ return 'no_chunks', 'Pending'
86
+ elif document.processing_status == ProcessingStatus.PROCESSING:
87
+ return 'partial', 'Processing...'
88
+ elif document.processing_status == ProcessingStatus.FAILED:
89
+ return 'none', 'Failed'
90
+
91
+ progress = self.get_vectorization_progress(document.id)
92
+ total = progress['total']
93
+ vectorized = progress['vectorized']
94
+ percentage = progress['percentage']
95
+
96
+ if total == 0:
97
+ return 'no_chunks', 'No chunks'
98
+ elif percentage == 100:
99
+ return 'completed', f'{vectorized}/{total} (100%)'
100
+ elif percentage > 0:
101
+ return 'partial', f'{vectorized}/{total} ({percentage}%)'
102
+ else:
103
+ return 'none', f'{vectorized}/{total} (0%)'
104
+ except Exception as e:
105
+ logger.error(f"Error getting vectorization status for document {document.id}: {e}")
106
+
107
+ # Try to provide more specific error information
108
+ if document.processing_status == ProcessingStatus.COMPLETED and document.chunks_count > 0:
109
+ return 'none', f'Error ({document.chunks_count} chunks)'
110
+ else:
111
+ return 'no_chunks', 'Error'
112
+
113
+ def find_duplicates(self, document):
114
+ """Find duplicate documents with same content hash."""
115
+ if not document.content_hash:
116
+ return self.none()
117
+
118
+ return self.get_queryset().filter(
119
+ user=document.user,
120
+ content_hash=document.content_hash
121
+ ).exclude(pk=document.pk)
122
+
123
+ def get_duplicate_info(self, document):
124
+ """Get duplicate information for admin display."""
125
+ if not document.content_hash:
126
+ return "No content hash"
127
+
128
+ duplicates = self.find_duplicates(document)
129
+
130
+ if not duplicates.exists():
131
+ return "✅ No duplicates found"
132
+
133
+ return {
134
+ 'count': duplicates.count(),
135
+ 'duplicates': list(duplicates[:3]) # Return first 3 for display
136
+ }
137
+
138
+ def check_duplicate_before_save(self, user, content, exclude_id=None):
139
+ """Check for duplicate content before saving. Returns (is_duplicate, existing_doc)."""
140
+ if not content:
141
+ return False, None
142
+
143
+ import hashlib
144
+ content_hash = hashlib.sha256(content.encode()).hexdigest()
145
+
146
+ # Use all_users() to bypass user filtering
147
+ query = self.all_users().filter(user=user, content_hash=content_hash)
148
+
149
+ if exclude_id:
150
+ query = query.exclude(pk=exclude_id)
151
+
152
+ existing_doc = query.first()
153
+ return existing_doc is not None, existing_doc
154
+
155
+
156
+ class DocumentChunkManager(models.Manager):
157
+ """Custom manager for DocumentChunk model."""
158
+
159
+ def for_user(self, user):
160
+ """Explicitly filter by specific user."""
161
+ return self.get_queryset().filter(user=user)
162
+
163
+ def all_users(self):
164
+ """Get unfiltered queryset (admin use)."""
165
+ return self.get_queryset()
166
+
167
+ def for_document(self, document_id: str):
168
+ """Get chunks for specific document."""
169
+ return self.get_queryset().filter(document_id=document_id)
170
+
171
+ def semantic_search(
172
+ self,
173
+ query_embedding: List[float],
174
+ limit: int = 5,
175
+ similarity_threshold: float = 0.7
176
+ ):
177
+ """Perform semantic search."""
178
+ from pgvector.django import CosineDistance
179
+
180
+ return self.get_queryset().annotate(
181
+ similarity=1 - CosineDistance('embedding', query_embedding)
182
+ ).filter(
183
+ similarity__gte=similarity_threshold
184
+ ).order_by('-similarity')[:limit]
185
+
186
+ def with_document_info(self):
187
+ """Get chunks with document information."""
188
+ return self.get_queryset().select_related('document')
189
+
190
+ def vectorized(self):
191
+ """Get only vectorized chunks (with non-zero embeddings)."""
192
+ # This is a simplified check - in practice you might want to check for non-zero vectors
193
+ return self.get_queryset().exclude(embedding__isnull=True)
194
+
195
+ def by_user_and_similarity(self, user, query_embedding: List[float], limit: int = 10):
196
+ """Get chunks for specific user with similarity search."""
197
+ from pgvector.django import CosineDistance
198
+
199
+ return self.get_queryset().filter(
200
+ user=user
201
+ ).annotate(
202
+ similarity=1 - CosineDistance('embedding', query_embedding)
203
+ ).order_by('-similarity')[:limit]