django-cfg 1.1.82__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. django_cfg/__init__.py +20 -448
  2. django_cfg/apps/accounts/README.md +3 -3
  3. django_cfg/apps/accounts/admin/__init__.py +0 -2
  4. django_cfg/apps/accounts/admin/activity.py +2 -9
  5. django_cfg/apps/accounts/admin/filters.py +0 -42
  6. django_cfg/apps/accounts/admin/inlines.py +8 -8
  7. django_cfg/apps/accounts/admin/otp.py +5 -5
  8. django_cfg/apps/accounts/admin/registration_source.py +1 -8
  9. django_cfg/apps/accounts/admin/user.py +12 -20
  10. django_cfg/apps/accounts/managers/user_manager.py +2 -129
  11. django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
  12. django_cfg/apps/accounts/models.py +3 -123
  13. django_cfg/apps/accounts/serializers/otp.py +40 -44
  14. django_cfg/apps/accounts/serializers/profile.py +0 -2
  15. django_cfg/apps/accounts/services/otp_service.py +98 -186
  16. django_cfg/apps/accounts/signals.py +25 -15
  17. django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
  18. django_cfg/apps/accounts/views/otp.py +35 -36
  19. django_cfg/apps/agents/README.md +129 -0
  20. django_cfg/apps/agents/__init__.py +68 -0
  21. django_cfg/apps/agents/admin/__init__.py +17 -0
  22. django_cfg/apps/agents/admin/execution_admin.py +460 -0
  23. django_cfg/apps/agents/admin/registry_admin.py +360 -0
  24. django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
  25. django_cfg/apps/agents/apps.py +29 -0
  26. django_cfg/apps/agents/core/__init__.py +20 -0
  27. django_cfg/apps/agents/core/agent.py +281 -0
  28. django_cfg/apps/agents/core/dependencies.py +154 -0
  29. django_cfg/apps/agents/core/exceptions.py +66 -0
  30. django_cfg/apps/agents/core/models.py +106 -0
  31. django_cfg/apps/agents/core/orchestrator.py +391 -0
  32. django_cfg/apps/agents/examples/__init__.py +3 -0
  33. django_cfg/apps/agents/examples/simple_example.py +161 -0
  34. django_cfg/apps/agents/integration/__init__.py +14 -0
  35. django_cfg/apps/agents/integration/middleware.py +80 -0
  36. django_cfg/apps/agents/integration/registry.py +345 -0
  37. django_cfg/apps/agents/integration/signals.py +50 -0
  38. django_cfg/apps/agents/management/__init__.py +3 -0
  39. django_cfg/apps/agents/management/commands/__init__.py +3 -0
  40. django_cfg/apps/agents/management/commands/create_agent.py +365 -0
  41. django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
  42. django_cfg/apps/agents/managers/__init__.py +23 -0
  43. django_cfg/apps/agents/managers/execution.py +236 -0
  44. django_cfg/apps/agents/managers/registry.py +254 -0
  45. django_cfg/apps/agents/managers/toolsets.py +496 -0
  46. django_cfg/apps/agents/migrations/0001_initial.py +286 -0
  47. django_cfg/apps/agents/migrations/__init__.py +5 -0
  48. django_cfg/apps/agents/models/__init__.py +15 -0
  49. django_cfg/apps/agents/models/execution.py +215 -0
  50. django_cfg/apps/agents/models/registry.py +220 -0
  51. django_cfg/apps/agents/models/toolsets.py +305 -0
  52. django_cfg/apps/agents/patterns/__init__.py +24 -0
  53. django_cfg/apps/agents/patterns/content_agents.py +234 -0
  54. django_cfg/apps/agents/toolsets/__init__.py +15 -0
  55. django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
  56. django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
  57. django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
  58. django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
  59. django_cfg/apps/agents/urls.py +46 -0
  60. django_cfg/apps/knowbase/README.md +150 -0
  61. django_cfg/apps/knowbase/__init__.py +27 -0
  62. django_cfg/apps/knowbase/admin/__init__.py +23 -0
  63. django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
  64. django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
  65. django_cfg/apps/knowbase/admin/document_admin.py +650 -0
  66. django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
  67. django_cfg/apps/knowbase/apps.py +81 -0
  68. django_cfg/apps/knowbase/config/README.md +176 -0
  69. django_cfg/apps/knowbase/config/__init__.py +51 -0
  70. django_cfg/apps/knowbase/config/constance_fields.py +186 -0
  71. django_cfg/apps/knowbase/config/constance_settings.py +200 -0
  72. django_cfg/apps/knowbase/config/settings.py +450 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +91 -19
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/twilio_service.py +2 -2
  198. django_cfg/modules/django_unfold/__init__.py +69 -0
  199. django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
  200. django_cfg/modules/django_unfold/dashboard.py +278 -0
  201. django_cfg/modules/django_unfold/icons/README.md +145 -0
  202. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  203. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  204. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  205. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  206. django_cfg/modules/django_unfold/models/config.py +601 -0
  207. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  208. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  209. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  210. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  211. django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
  212. django_cfg/modules/django_unfold/utils.py +140 -0
  213. django_cfg/registry/__init__.py +23 -0
  214. django_cfg/registry/core.py +61 -0
  215. django_cfg/registry/exceptions.py +11 -0
  216. django_cfg/registry/modules.py +12 -0
  217. django_cfg/registry/services.py +26 -0
  218. django_cfg/registry/third_party.py +52 -0
  219. django_cfg/routing/__init__.py +19 -0
  220. django_cfg/routing/callbacks.py +198 -0
  221. django_cfg/routing/routers.py +48 -0
  222. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  223. django_cfg/templatetags/__init__.py +0 -0
  224. django_cfg/templatetags/django_cfg.py +33 -0
  225. django_cfg/urls.py +33 -0
  226. django_cfg/utils/path_resolution.py +1 -1
  227. django_cfg/utils/smart_defaults.py +7 -61
  228. django_cfg/utils/toolkit.py +663 -0
  229. {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/METADATA +83 -86
  230. django_cfg-1.2.1.dist-info/RECORD +441 -0
  231. django_cfg/archive/django_sample.zip +0 -0
  232. django_cfg/models/unfold.py +0 -271
  233. django_cfg/modules/unfold/__init__.py +0 -29
  234. django_cfg/modules/unfold/dashboard.py +0 -318
  235. django_cfg/pyproject.toml +0 -370
  236. django_cfg/routers.py +0 -83
  237. django_cfg-1.1.82.dist-info/RECORD +0 -278
  238. /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
  239. /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
  240. /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
  241. /django_cfg/{version_check.py → utils/version_check.py} +0 -0
  242. {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/WHEEL +0 -0
  243. {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/entry_points.txt +0 -0
  244. {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,157 @@
1
+ """
2
+ Chat models for RAG-powered conversations.
3
+ """
4
+
5
+ from django.db import models
6
+ from .base import UserScopedModel
7
+
8
+
9
+ class ChatSession(UserScopedModel):
10
+ """User chat session for conversation tracking."""
11
+
12
+ # Custom managers
13
+ from ..managers.chat import ChatSessionManager
14
+ objects = ChatSessionManager()
15
+
16
+ title = models.CharField(
17
+ max_length=255,
18
+ blank=True,
19
+ help_text="Session title (auto-generated if empty)"
20
+ )
21
+ is_active = models.BooleanField(
22
+ default=True,
23
+ help_text="Whether session accepts new messages"
24
+ )
25
+
26
+ # Session statistics
27
+ messages_count = models.PositiveIntegerField(default=0)
28
+ total_tokens_used = models.PositiveIntegerField(default=0)
29
+ total_cost_usd = models.FloatField(
30
+ default=0.0,
31
+ help_text="Total session cost for monitoring"
32
+ )
33
+
34
+ # Configuration
35
+ model_name = models.CharField(
36
+ max_length=100,
37
+ default="openai/gpt-4o-mini",
38
+ help_text="LLM model used for this session"
39
+ )
40
+ temperature = models.FloatField(
41
+ default=0.7,
42
+ help_text="Temperature setting for LLM"
43
+ )
44
+ max_context_chunks = models.PositiveIntegerField(
45
+ default=5,
46
+ help_text="Maximum chunks to include in context"
47
+ )
48
+
49
+ class Meta:
50
+ db_table = 'django_cfg_knowbase_chat_sessions'
51
+ indexes = [
52
+ models.Index(fields=['user', '-created_at']),
53
+ models.Index(fields=['is_active']),
54
+ ]
55
+
56
+ def __str__(self) -> str:
57
+ return self.title or f"Session {self.id}"
58
+
59
+ def generate_title_if_empty(self) -> None:
60
+ """Auto-generate title based on first message."""
61
+ if not self.title and self.messages.exists():
62
+ first_message = self.messages.filter(
63
+ role='user'
64
+ ).first()
65
+ if first_message:
66
+ # Take first 50 characters as title
67
+ self.title = first_message.content[:50] + "..."
68
+ self.save(update_fields=['title'])
69
+
70
+ def archive(self) -> None:
71
+ """Archive (deactivate) this session."""
72
+ self.is_active = False
73
+ self.save(update_fields=['is_active'])
74
+
75
+ def activate(self) -> None:
76
+ """Activate this session."""
77
+ self.is_active = True
78
+ self.save(update_fields=['is_active'])
79
+
80
+ @property
81
+ def is_archived(self) -> bool:
82
+ """Check if session is archived."""
83
+ return not self.is_active
84
+
85
+
86
+ class ChatMessage(UserScopedModel):
87
+ """Individual chat message with context tracking."""
88
+
89
+ # Custom managers
90
+ from ..managers.chat import ChatMessageManager
91
+ objects = ChatMessageManager()
92
+
93
+ class MessageRole(models.TextChoices):
94
+ USER = 'user', 'User'
95
+ ASSISTANT = 'assistant', 'Assistant'
96
+ SYSTEM = 'system', 'System'
97
+
98
+ session = models.ForeignKey(
99
+ ChatSession,
100
+ on_delete=models.CASCADE,
101
+ related_name='messages',
102
+ help_text="Parent chat session"
103
+ )
104
+
105
+ role = models.CharField(
106
+ max_length=10,
107
+ choices=MessageRole.choices,
108
+ help_text="Message sender role"
109
+ )
110
+ content = models.TextField(
111
+ help_text="Message content"
112
+ )
113
+
114
+ # Context tracking
115
+ context_chunks = models.JSONField(
116
+ default=list,
117
+ help_text="IDs of chunks used for context"
118
+ )
119
+
120
+ # Usage tracking (for monitoring, not billing)
121
+ tokens_used = models.PositiveIntegerField(
122
+ default=0,
123
+ help_text="Tokens used for this message"
124
+ )
125
+ cost_usd = models.FloatField(
126
+ default=0.0,
127
+ help_text="Cost in USD for this message"
128
+ )
129
+ processing_time_ms = models.PositiveIntegerField(
130
+ default=0,
131
+ help_text="Processing time in milliseconds"
132
+ )
133
+
134
+ # Response metadata
135
+ model_name = models.CharField(
136
+ max_length=100,
137
+ blank=True,
138
+ help_text="Model used for response generation"
139
+ )
140
+ finish_reason = models.CharField(
141
+ max_length=20,
142
+ blank=True,
143
+ help_text="Why the model stopped generating"
144
+ )
145
+
146
+ class Meta:
147
+ db_table = 'django_cfg_knowbase_chat_messages'
148
+ indexes = [
149
+ models.Index(fields=['session', '-created_at']),
150
+ models.Index(fields=['role']),
151
+ models.Index(fields=['-created_at']),
152
+ ]
153
+ ordering = ['created_at']
154
+
155
+ def __str__(self) -> str:
156
+ preview = self.content[:50] + "..." if len(self.content) > 50 else self.content
157
+ return f"{self.role}: {preview}"
@@ -0,0 +1,267 @@
1
+ """
2
+ Document models with pgvector support.
3
+ """
4
+
5
+ from django.db import models
6
+ from pgvector.django import VectorField
7
+ from typing import Optional, List
8
+ from .base import UserScopedModel, ProcessingStatus, TimestampedModel
9
+
10
+
11
+ class DocumentCategory(TimestampedModel):
12
+ """Document category for organization and access control."""
13
+
14
+ name = models.CharField(
15
+ max_length=255,
16
+ unique=True,
17
+ help_text="Category name"
18
+ )
19
+ description = models.TextField(
20
+ blank=True,
21
+ help_text="Category description"
22
+ )
23
+ is_public = models.BooleanField(
24
+ default=True,
25
+ help_text="Whether documents in this category are publicly accessible"
26
+ )
27
+
28
+ class Meta:
29
+ db_table = 'django_cfg_knowbase_document_categories'
30
+ verbose_name = 'Document Category'
31
+ verbose_name_plural = 'Document Categories'
32
+ ordering = ['name']
33
+
34
+ def __str__(self) -> str:
35
+ return f"{self.name} ({'Public' if self.is_public else 'Private'})"
36
+
37
+
38
+ class Document(UserScopedModel):
39
+ """Knowledge document with processing status tracking."""
40
+
41
+ # Custom managers
42
+ from ..managers.document import DocumentManager
43
+ objects = DocumentManager()
44
+
45
+ title = models.CharField(
46
+ max_length=512,
47
+ help_text="Document title"
48
+ )
49
+ content = models.TextField(
50
+ help_text="Full document content"
51
+ )
52
+ # Multiple categories field
53
+ categories = models.ManyToManyField(
54
+ DocumentCategory,
55
+ blank=True,
56
+ related_name='documents',
57
+ help_text="Document categories (supports multiple)"
58
+ )
59
+ is_public = models.BooleanField(
60
+ default=True,
61
+ help_text="Whether this document is publicly accessible"
62
+ )
63
+ content_hash = models.CharField(
64
+ max_length=64,
65
+ db_index=True,
66
+ help_text="SHA-256 hash for duplicate detection"
67
+ )
68
+ file_type = models.CharField(
69
+ max_length=100,
70
+ default="text/plain",
71
+ help_text="MIME type of original file"
72
+ )
73
+ file_size = models.PositiveIntegerField(
74
+ default=0,
75
+ help_text="Original file size in bytes"
76
+ )
77
+
78
+ # Processing status
79
+ processing_status = models.CharField(
80
+ max_length=20,
81
+ choices=ProcessingStatus.choices,
82
+ default=ProcessingStatus.PENDING,
83
+ db_index=True
84
+ )
85
+ processing_started_at = models.DateTimeField(null=True, blank=True)
86
+ processing_completed_at = models.DateTimeField(null=True, blank=True)
87
+ processing_error = models.TextField(blank=True, default="")
88
+
89
+ # Chunk statistics
90
+ chunks_count = models.PositiveIntegerField(default=0)
91
+ total_tokens = models.PositiveIntegerField(default=0)
92
+
93
+ # Cost tracking for monitoring
94
+ total_cost_usd = models.FloatField(
95
+ default=0.0,
96
+ help_text="Total processing cost in USD"
97
+ )
98
+
99
+ # Metadata
100
+ metadata = models.JSONField(
101
+ default=dict,
102
+ blank=True,
103
+ null=True,
104
+ help_text="Additional document metadata"
105
+ )
106
+
107
+ class Meta:
108
+ db_table = 'django_cfg_knowbase_documents'
109
+ indexes = [
110
+ models.Index(fields=['user', 'processing_status']),
111
+ models.Index(fields=['content_hash']),
112
+ models.Index(fields=['-processing_completed_at']),
113
+ models.Index(fields=['is_public', '-created_at']), # For multiple categories queries
114
+ ]
115
+ constraints = [
116
+ models.UniqueConstraint(
117
+ fields=['user', 'content_hash'],
118
+ name='unique_user_document'
119
+ )
120
+ ]
121
+
122
+ def save(self, *args, **kwargs):
123
+ """Override save to generate content_hash if not provided."""
124
+ if not self.content_hash and self.content:
125
+ import hashlib
126
+ self.content_hash = hashlib.sha256(self.content.encode()).hexdigest()
127
+
128
+ # Set file_size if not provided
129
+ if not self.file_size and self.content:
130
+ self.file_size = len(self.content.encode('utf-8'))
131
+
132
+ super().save(*args, **kwargs)
133
+
134
+ def __str__(self) -> str:
135
+ return f"{self.title} ({self.user.username})"
136
+
137
+ @property
138
+ def is_processed(self) -> bool:
139
+ """Check if document processing is completed."""
140
+ return self.processing_status == ProcessingStatus.COMPLETED
141
+
142
+ @property
143
+ def processing_duration(self) -> Optional[float]:
144
+ """Calculate processing duration in seconds."""
145
+ if self.processing_started_at and self.processing_completed_at:
146
+ delta = self.processing_completed_at - self.processing_started_at
147
+ return delta.total_seconds()
148
+ return None
149
+
150
+ @property
151
+ def is_publicly_accessible(self) -> bool:
152
+ """Check if document is publicly accessible (document and at least one category must be public)."""
153
+ if not self.is_public:
154
+ return False
155
+
156
+ # If document has categories, at least one must be public
157
+ if self.categories.exists():
158
+ return self.categories.filter(is_public=True).exists()
159
+
160
+ # If no categories assigned, document is public by default
161
+ return True
162
+
163
+ def get_all_categories(self):
164
+ """Get all categories for this document."""
165
+ return list(self.categories.all())
166
+
167
+ def add_category(self, category):
168
+ """Add a category to this document."""
169
+ self.categories.add(category)
170
+
171
+ def remove_category(self, category):
172
+ """Remove a category from this document."""
173
+ self.categories.remove(category)
174
+
175
+ def set_categories(self, categories_list):
176
+ """Set multiple categories for this document."""
177
+ self.categories.set(categories_list)
178
+
179
+
180
+ class DocumentChunk(UserScopedModel):
181
+ """Text chunk with vector embedding for semantic search."""
182
+
183
+ # Custom managers
184
+ from ..managers.document import DocumentChunkManager
185
+ objects = DocumentChunkManager()
186
+
187
+ document = models.ForeignKey(
188
+ Document,
189
+ on_delete=models.CASCADE,
190
+ related_name='chunks',
191
+ help_text="Parent document"
192
+ )
193
+ content = models.TextField(
194
+ help_text="Chunk text content"
195
+ )
196
+ chunk_index = models.PositiveIntegerField(
197
+ help_text="Sequential chunk number within document"
198
+ )
199
+
200
+ # Vector embedding (1536 dimensions for OpenAI text-embedding-ada-002)
201
+ embedding = VectorField(
202
+ dimensions=1536,
203
+ help_text="Vector embedding for semantic search"
204
+ )
205
+
206
+ # Chunk statistics
207
+ token_count = models.PositiveIntegerField(
208
+ default=0,
209
+ help_text="Number of tokens in chunk"
210
+ )
211
+ character_count = models.PositiveIntegerField(
212
+ default=0,
213
+ help_text="Number of characters in chunk"
214
+ )
215
+
216
+ # Processing metadata
217
+ embedding_model = models.CharField(
218
+ max_length=100,
219
+ default="text-embedding-ada-002",
220
+ help_text="Model used for embedding generation"
221
+ )
222
+ embedding_cost = models.FloatField(
223
+ default=0.0,
224
+ help_text="Cost in USD for embedding generation"
225
+ )
226
+
227
+ # Additional metadata
228
+ metadata = models.JSONField(
229
+ default=dict,
230
+ blank=True,
231
+ null=True,
232
+ help_text="Chunk-specific metadata"
233
+ )
234
+
235
+ class Meta:
236
+ db_table = 'django_cfg_knowbase_document_chunks'
237
+ indexes = [
238
+ models.Index(fields=['user']),
239
+ models.Index(fields=['document', 'chunk_index']),
240
+ ]
241
+ constraints = [
242
+ models.UniqueConstraint(
243
+ fields=['document', 'chunk_index'],
244
+ name='unique_document_chunk'
245
+ )
246
+ ]
247
+ ordering = ['document', 'chunk_index']
248
+
249
+ def __str__(self) -> str:
250
+ return f"Chunk {self.chunk_index} of {self.document.title}"
251
+
252
+ @classmethod
253
+ def semantic_search(
254
+ cls,
255
+ user,
256
+ query_embedding: List[float],
257
+ limit: int = 5,
258
+ similarity_threshold: float = 0.7
259
+ ):
260
+ """Perform semantic search using pgvector."""
261
+ from pgvector.django import CosineDistance
262
+
263
+ return cls.objects.filter(user=user).annotate(
264
+ similarity=1 - CosineDistance('embedding', query_embedding)
265
+ ).filter(
266
+ similarity__gte=similarity_threshold
267
+ ).order_by('-similarity')[:limit]