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,99 @@
1
+ """
2
+ Validation utilities for the knowbase app.
3
+ """
4
+
5
+ import math
6
+ from typing import Union, Optional
7
+
8
+
9
+ def is_valid_float(value: Union[float, int, None]) -> bool:
10
+ """
11
+ Check if a value is a valid float for JSON serialization.
12
+
13
+ Args:
14
+ value: The value to check
15
+
16
+ Returns:
17
+ True if the value is a valid float, False otherwise
18
+ """
19
+ if value is None:
20
+ return False
21
+
22
+ try:
23
+ float_value = float(value)
24
+ return not (math.isnan(float_value) or math.isinf(float_value))
25
+ except (ValueError, TypeError):
26
+ return False
27
+
28
+
29
+ def safe_float(value: Union[float, int, None], default: float = 0.0) -> float:
30
+ """
31
+ Convert a value to a safe float, replacing invalid values with default.
32
+
33
+ Args:
34
+ value: The value to convert
35
+ default: Default value to use for invalid inputs
36
+
37
+ Returns:
38
+ A valid float value
39
+ """
40
+ if value is None:
41
+ return default
42
+
43
+ try:
44
+ float_value = float(value)
45
+ if math.isnan(float_value) or math.isinf(float_value):
46
+ return default
47
+ return float_value
48
+ except (ValueError, TypeError):
49
+ return default
50
+
51
+
52
+ def validate_similarity_score(similarity: Union[float, int, None]) -> Optional[float]:
53
+ """
54
+ Validate and normalize a similarity score.
55
+
56
+ Args:
57
+ similarity: The similarity score to validate
58
+
59
+ Returns:
60
+ Valid similarity score or None if invalid
61
+ """
62
+ if not is_valid_float(similarity):
63
+ return None
64
+
65
+ score = float(similarity)
66
+
67
+ # Clamp to valid range [0.0, 1.0] for similarity scores
68
+ if score < 0.0:
69
+ return 0.0
70
+ elif score > 1.0:
71
+ return 1.0
72
+
73
+ return score
74
+
75
+
76
+ def clean_search_results(results: list) -> list:
77
+ """
78
+ Clean search results by removing entries with invalid similarity scores.
79
+
80
+ Args:
81
+ results: List of search result dictionaries
82
+
83
+ Returns:
84
+ Cleaned list with valid similarity scores
85
+ """
86
+ cleaned_results = []
87
+
88
+ for result in results:
89
+ if 'similarity' not in result:
90
+ continue
91
+
92
+ similarity = validate_similarity_score(result['similarity'])
93
+ if similarity is not None:
94
+ # Create a copy and update similarity
95
+ cleaned_result = result.copy()
96
+ cleaned_result['similarity'] = similarity
97
+ cleaned_results.append(cleaned_result)
98
+
99
+ return cleaned_results
@@ -0,0 +1,28 @@
1
+ """
2
+ Knowledge Base API Views
3
+
4
+ DRF ViewSets for REST API endpoints.
5
+ """
6
+
7
+ from .document_views import *
8
+ from .chat_views import *
9
+ from .public_views import *
10
+ from .archive_views import *
11
+
12
+ __all__ = [
13
+ # Document views
14
+ 'DocumentViewSet',
15
+
16
+ # Public views
17
+ 'PublicDocumentViewSet',
18
+ 'PublicCategoryViewSet',
19
+
20
+ # Chat views
21
+ 'ChatViewSet',
22
+ 'ChatSessionViewSet',
23
+
24
+ # Archive views
25
+ 'DocumentArchiveViewSet',
26
+ 'ArchiveItemViewSet',
27
+ 'ArchiveItemChunkViewSet',
28
+ ]
@@ -0,0 +1,469 @@
1
+ """
2
+ Document archive management API views.
3
+ """
4
+
5
+ from rest_framework import status, viewsets, parsers
6
+ from rest_framework.decorators import action
7
+ from rest_framework.response import Response
8
+ from rest_framework.permissions import IsAuthenticated, IsAdminUser
9
+ from django_ratelimit.decorators import ratelimit
10
+ from django.utils.decorators import method_decorator
11
+ from django.db import models
12
+ from drf_spectacular.utils import extend_schema, OpenApiExample, OpenApiParameter
13
+ from typing import Dict, Any
14
+ import logging
15
+
16
+ from ..models.archive import DocumentArchive, ArchiveItem, ArchiveItemChunk
17
+ from ..services.archive import (
18
+ DocumentArchiveService,
19
+ ArchiveVectorizationService,
20
+ ArchiveProcessingError,
21
+ ArchiveValidationError
22
+ )
23
+ from ..serializers.archive_serializers import (
24
+ DocumentArchiveCreateSerializer,
25
+ DocumentArchiveSerializer,
26
+ DocumentArchiveDetailSerializer,
27
+ DocumentArchiveListSerializer,
28
+ ArchiveItemSerializer,
29
+ ArchiveItemDetailSerializer,
30
+ ArchiveItemChunkSerializer,
31
+ ArchiveItemChunkDetailSerializer,
32
+ ArchiveProcessingResultSerializer,
33
+ ArchiveSearchRequestSerializer,
34
+ ArchiveSearchResultSerializer,
35
+ ArchiveStatisticsSerializer,
36
+ VectorizationStatisticsSerializer,
37
+ ArchiveUploadSerializer,
38
+ ChunkRevectorizationRequestSerializer,
39
+ VectorizationResultSerializer
40
+ )
41
+ from .base import BaseKnowledgeViewSet
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ class DocumentArchiveViewSet(BaseKnowledgeViewSet):
47
+ """Document archive management endpoints - Admin only."""
48
+
49
+ queryset = DocumentArchive.objects.all()
50
+ serializer_class = DocumentArchiveSerializer
51
+ service_class = DocumentArchiveService
52
+ permission_classes = [IsAuthenticated, IsAdminUser]
53
+ parser_classes = [parsers.MultiPartParser, parsers.JSONParser]
54
+
55
+ def get_serializer_class(self):
56
+ """Return appropriate serializer based on action."""
57
+ if self.action == 'create':
58
+ return DocumentArchiveCreateSerializer
59
+ elif self.action == 'list':
60
+ return DocumentArchiveListSerializer
61
+ elif self.action in ['retrieve', 'items', 'file_tree']:
62
+ return DocumentArchiveDetailSerializer
63
+ elif self.action == 'upload':
64
+ return ArchiveUploadSerializer
65
+ elif self.action == 'processing_result':
66
+ return ArchiveProcessingResultSerializer
67
+ elif self.action == 'statistics':
68
+ return ArchiveStatisticsSerializer
69
+ return DocumentArchiveSerializer
70
+
71
+ @extend_schema(
72
+ summary="Upload and process archive",
73
+ description="Upload archive file and process it synchronously",
74
+ request={
75
+ 'multipart/form-data': {
76
+ 'type': 'object',
77
+ 'properties': {
78
+ 'file': {'type': 'string', 'format': 'binary'},
79
+ 'title': {'type': 'string'},
80
+ 'description': {'type': 'string'},
81
+ 'category_ids': {'type': 'array', 'items': {'type': 'string'}},
82
+ 'is_public': {'type': 'boolean'},
83
+ 'process_immediately': {'type': 'boolean'}
84
+ }
85
+ }
86
+ },
87
+ responses={
88
+ 201: ArchiveProcessingResultSerializer,
89
+ 400: "Validation errors",
90
+ 413: "File too large",
91
+ 429: "Rate limit exceeded"
92
+ }
93
+ )
94
+ @method_decorator(ratelimit(key='user', rate='5/hour', method='POST'))
95
+ def create(self, request, *args, **kwargs):
96
+ """Upload and process archive file."""
97
+
98
+ # Validate file upload
99
+ file_serializer = ArchiveUploadSerializer(data=request.FILES)
100
+ if not file_serializer.is_valid():
101
+ return Response(
102
+ file_serializer.errors,
103
+ status=status.HTTP_400_BAD_REQUEST
104
+ )
105
+
106
+ # Validate metadata
107
+ metadata_serializer = DocumentArchiveCreateSerializer(data=request.data)
108
+ if not metadata_serializer.is_valid():
109
+ return Response(
110
+ metadata_serializer.errors,
111
+ status=status.HTTP_400_BAD_REQUEST
112
+ )
113
+
114
+ try:
115
+ service = self.get_service()
116
+ uploaded_file = file_serializer.validated_data['file']
117
+
118
+ # Process archive
119
+ result = service.create_and_process_archive(
120
+ uploaded_file=uploaded_file,
121
+ request_data=metadata_serializer.validated_data
122
+ )
123
+
124
+ # Return processing result
125
+ result_serializer = ArchiveProcessingResultSerializer(result)
126
+ return Response(
127
+ result_serializer.data,
128
+ status=status.HTTP_201_CREATED
129
+ )
130
+
131
+ except ArchiveValidationError as e:
132
+ return Response(
133
+ {
134
+ 'error': 'Validation Error',
135
+ 'message': e.message,
136
+ 'code': e.code,
137
+ 'details': e.details
138
+ },
139
+ status=status.HTTP_400_BAD_REQUEST
140
+ )
141
+ except ArchiveProcessingError as e:
142
+ return Response(
143
+ {
144
+ 'error': 'Processing Error',
145
+ 'message': e.message,
146
+ 'code': e.code,
147
+ 'details': e.details
148
+ },
149
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
150
+ )
151
+ except Exception as e:
152
+ logger.error(f"Archive upload error: {e}", exc_info=True)
153
+ return Response(
154
+ {
155
+ 'error': 'Internal Server Error',
156
+ 'message': 'An unexpected error occurred'
157
+ },
158
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
159
+ )
160
+
161
+ @extend_schema(
162
+ summary="Get archive items",
163
+ description="Get all items in the archive",
164
+ responses={200: ArchiveItemSerializer(many=True)}
165
+ )
166
+ @action(detail=True, methods=['get'])
167
+ def items(self, request, pk=None):
168
+ """Get archive items."""
169
+
170
+ archive = self.get_object()
171
+ items = archive.items.all().order_by('relative_path')
172
+
173
+ serializer = ArchiveItemSerializer(items, many=True)
174
+ return Response(serializer.data)
175
+
176
+ @extend_schema(
177
+ summary="Get archive file tree",
178
+ description="Get hierarchical file tree structure",
179
+ responses={200: {'type': 'object'}}
180
+ )
181
+ @action(detail=True, methods=['get'])
182
+ def file_tree(self, request, pk=None):
183
+ """Get archive file tree structure."""
184
+
185
+ archive = self.get_object()
186
+ file_tree = archive.get_file_tree()
187
+
188
+ return Response({'file_tree': file_tree})
189
+
190
+ @extend_schema(
191
+ summary="Search archive chunks",
192
+ description="Semantic search within archive chunks",
193
+ request=ArchiveSearchRequestSerializer,
194
+ responses={200: ArchiveSearchResultSerializer(many=True)}
195
+ )
196
+ @action(detail=True, methods=['post'])
197
+ def search(self, request, pk=None):
198
+ """Search within archive chunks."""
199
+
200
+ archive = self.get_object()
201
+
202
+ # Validate search request
203
+ search_serializer = ArchiveSearchRequestSerializer(data=request.data)
204
+ if not search_serializer.is_valid():
205
+ return Response(
206
+ search_serializer.errors,
207
+ status=status.HTTP_400_BAD_REQUEST
208
+ )
209
+
210
+ search_data = search_serializer.validated_data
211
+
212
+ try:
213
+ # Perform search using vectorization service
214
+ vectorization_service = ArchiveVectorizationService(request.user)
215
+
216
+ # Generate query embedding
217
+ from django_cfg.modules.django_llm.llm.client import LLMClient
218
+ from django_cfg.apps.knowbase.config.settings import get_openai_api_key, get_openrouter_api_key, get_cache_settings
219
+ cache_settings = get_cache_settings()
220
+ llm_client = LLMClient(
221
+ apikey_openai=get_openai_api_key(),
222
+ apikey_openrouter=get_openrouter_api_key(),
223
+ cache_dir=cache_settings.cache_dir,
224
+ cache_ttl=cache_settings.cache_ttl,
225
+ max_cache_size=cache_settings.max_cache_size
226
+ )
227
+ # Generate query embedding with specified model
228
+ from django_cfg.apps.knowbase.utils.chunk_settings import get_embedding_model
229
+ embedding_model = get_embedding_model()
230
+ embedding_result = llm_client.generate_embedding(
231
+ text=search_data['query'],
232
+ model=embedding_model
233
+ )
234
+
235
+ # Search chunks using manager's semantic_search method
236
+ chunks = ArchiveItemChunk.objects.semantic_search(
237
+ query_embedding=embedding_result.embedding,
238
+ limit=search_data.get('limit', 10),
239
+ similarity_threshold=search_data.get('similarity_threshold', 0.7),
240
+ content_types=search_data.get('content_types'),
241
+ languages=search_data.get('languages'),
242
+ # Filter by archive and user
243
+ archive=archive,
244
+ user=request.user,
245
+ chunk_types=search_data.get('chunk_types')
246
+ )
247
+
248
+ # Build search results
249
+ results = []
250
+ for chunk in chunks:
251
+ result_data = {
252
+ 'chunk': chunk,
253
+ 'similarity_score': getattr(chunk, 'similarity', 0.0),
254
+ 'context_summary': chunk.get_context_summary(),
255
+ 'archive_info': {
256
+ 'id': str(archive.id),
257
+ 'title': archive.title
258
+ },
259
+ 'item_info': {
260
+ 'id': str(chunk.item.id),
261
+ 'relative_path': chunk.item.relative_path,
262
+ 'content_type': chunk.item.content_type
263
+ }
264
+ }
265
+ results.append(result_data)
266
+
267
+ serializer = ArchiveSearchResultSerializer(results, many=True)
268
+ return Response(serializer.data)
269
+
270
+ except Exception as e:
271
+ logger.error(f"Archive search error: {e}", exc_info=True)
272
+ return Response(
273
+ {'error': 'Search failed', 'message': str(e)},
274
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
275
+ )
276
+
277
+ @extend_schema(
278
+ summary="Get archive statistics",
279
+ description="Get processing and vectorization statistics",
280
+ responses={200: ArchiveStatisticsSerializer}
281
+ )
282
+ @action(detail=False, methods=['get'])
283
+ def statistics(self, request):
284
+ """Get user's archive statistics."""
285
+
286
+ service = self.get_service()
287
+
288
+ # Get archive statistics from manager
289
+ from ..managers.archive import DocumentArchiveManager
290
+ archive_manager = DocumentArchiveManager()
291
+ archive_manager.model = DocumentArchive
292
+
293
+ stats = archive_manager.get_processing_statistics(user=request.user)
294
+
295
+ serializer = ArchiveStatisticsSerializer(stats)
296
+ return Response(serializer.data)
297
+
298
+ @extend_schema(
299
+ summary="Get vectorization statistics",
300
+ description="Get vectorization statistics for archives",
301
+ responses={200: VectorizationStatisticsSerializer}
302
+ )
303
+ @action(detail=False, methods=['get'])
304
+ def vectorization_stats(self, request):
305
+ """Get vectorization statistics."""
306
+
307
+ vectorization_service = ArchiveVectorizationService(request.user)
308
+ stats = vectorization_service.get_vectorization_statistics()
309
+
310
+ serializer = VectorizationStatisticsSerializer(stats)
311
+ return Response(serializer.data)
312
+
313
+ @extend_schema(
314
+ summary="Re-vectorize chunks",
315
+ description="Re-vectorize specific chunks",
316
+ request=ChunkRevectorizationRequestSerializer,
317
+ responses={200: VectorizationResultSerializer}
318
+ )
319
+ @action(detail=False, methods=['post'])
320
+ def revectorize(self, request):
321
+ """Re-vectorize specific chunks."""
322
+
323
+ serializer = ChunkRevectorizationRequestSerializer(data=request.data)
324
+ if not serializer.is_valid():
325
+ return Response(
326
+ serializer.errors,
327
+ status=status.HTTP_400_BAD_REQUEST
328
+ )
329
+
330
+ try:
331
+ vectorization_service = ArchiveVectorizationService(request.user)
332
+ result = vectorization_service.revectorize_chunks(
333
+ chunk_ids=serializer.validated_data['chunk_ids'],
334
+ force=serializer.validated_data['force']
335
+ )
336
+
337
+ result_serializer = VectorizationResultSerializer(result)
338
+ return Response(result_serializer.data)
339
+
340
+ except Exception as e:
341
+ logger.error(f"Re-vectorization error: {e}", exc_info=True)
342
+ return Response(
343
+ {'error': 'Re-vectorization failed', 'message': str(e)},
344
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
345
+ )
346
+
347
+
348
+ class ArchiveItemViewSet(BaseKnowledgeViewSet):
349
+ """Archive item management endpoints - Admin only."""
350
+
351
+ queryset = ArchiveItem.objects.all()
352
+ serializer_class = ArchiveItemSerializer
353
+ permission_classes = [IsAuthenticated, IsAdminUser]
354
+
355
+ def get_queryset(self):
356
+ """Filter items by user and optional archive."""
357
+ queryset = super().get_queryset()
358
+
359
+ archive_id = self.request.query_params.get('archive_id')
360
+ if archive_id:
361
+ queryset = queryset.filter(archive_id=archive_id)
362
+
363
+ return queryset
364
+
365
+ def get_serializer_class(self):
366
+ """Return appropriate serializer based on action."""
367
+ if self.action in ['retrieve', 'content']:
368
+ return ArchiveItemDetailSerializer
369
+ return ArchiveItemSerializer
370
+
371
+ @extend_schema(
372
+ summary="Get item content",
373
+ description="Get full content of archive item",
374
+ responses={200: ArchiveItemDetailSerializer}
375
+ )
376
+ @action(detail=True, methods=['get'])
377
+ def content(self, request, pk=None):
378
+ """Get item content."""
379
+
380
+ item = self.get_object()
381
+ serializer = ArchiveItemDetailSerializer(item)
382
+ return Response(serializer.data)
383
+
384
+ @extend_schema(
385
+ summary="Get item chunks",
386
+ description="Get all chunks for this item",
387
+ responses={200: ArchiveItemChunkSerializer(many=True)}
388
+ )
389
+ @action(detail=True, methods=['get'])
390
+ def chunks(self, request, pk=None):
391
+ """Get item chunks."""
392
+
393
+ item = self.get_object()
394
+ chunks = item.chunks.all().order_by('chunk_index')
395
+
396
+ serializer = ArchiveItemChunkSerializer(chunks, many=True)
397
+ return Response(serializer.data)
398
+
399
+
400
+ class ArchiveItemChunkViewSet(BaseKnowledgeViewSet):
401
+ """Archive item chunk management endpoints - Admin only."""
402
+
403
+ queryset = ArchiveItemChunk.objects.all()
404
+ serializer_class = ArchiveItemChunkSerializer
405
+ permission_classes = [IsAuthenticated, IsAdminUser]
406
+
407
+ def get_queryset(self):
408
+ """Filter chunks by user and optional filters."""
409
+ queryset = super().get_queryset()
410
+
411
+ # Filter by archive
412
+ archive_id = self.request.query_params.get('archive_id')
413
+ if archive_id:
414
+ queryset = queryset.filter(archive_id=archive_id)
415
+
416
+ # Filter by item
417
+ item_id = self.request.query_params.get('item_id')
418
+ if item_id:
419
+ queryset = queryset.filter(item_id=item_id)
420
+
421
+ # Filter by chunk type
422
+ chunk_type = self.request.query_params.get('chunk_type')
423
+ if chunk_type:
424
+ queryset = queryset.filter(chunk_type=chunk_type)
425
+
426
+ return queryset
427
+
428
+ def get_serializer_class(self):
429
+ """Return appropriate serializer based on action."""
430
+ if self.action in ['retrieve', 'context']:
431
+ return ArchiveItemChunkDetailSerializer
432
+ return ArchiveItemChunkSerializer
433
+
434
+ @extend_schema(
435
+ summary="Get chunk context",
436
+ description="Get full context metadata for chunk",
437
+ responses={200: ArchiveItemChunkDetailSerializer}
438
+ )
439
+ @action(detail=True, methods=['get'])
440
+ def context(self, request, pk=None):
441
+ """Get chunk context metadata."""
442
+
443
+ chunk = self.get_object()
444
+ serializer = ArchiveItemChunkDetailSerializer(chunk)
445
+ return Response(serializer.data)
446
+
447
+ @extend_schema(
448
+ summary="Vectorize chunk",
449
+ description="Generate embedding for specific chunk",
450
+ responses={200: {'type': 'object'}}
451
+ )
452
+ @action(detail=True, methods=['post'])
453
+ def vectorize(self, request, pk=None):
454
+ """Vectorize specific chunk."""
455
+
456
+ chunk = self.get_object()
457
+
458
+ try:
459
+ vectorization_service = ArchiveVectorizationService(request.user)
460
+ result = vectorization_service.vectorize_single_chunk(str(chunk.id))
461
+
462
+ return Response(result)
463
+
464
+ except Exception as e:
465
+ logger.error(f"Chunk vectorization error: {e}", exc_info=True)
466
+ return Response(
467
+ {'error': 'Vectorization failed', 'message': str(e)},
468
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
469
+ )
@@ -0,0 +1,49 @@
1
+ """
2
+ Base views for knowledge base API.
3
+ """
4
+
5
+ from rest_framework import viewsets, status
6
+ from rest_framework.response import Response
7
+ from rest_framework.permissions import IsAuthenticated
8
+ from django_ratelimit.decorators import ratelimit
9
+ from django.utils.decorators import method_decorator
10
+ from typing import Type, Any
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class BaseKnowledgeViewSet(viewsets.ModelViewSet):
17
+ """Base ViewSet with common knowledge base functionality."""
18
+
19
+ permission_classes = [IsAuthenticated]
20
+ lookup_field = 'pk'
21
+
22
+ def get_queryset(self):
23
+ """Filter queryset by authenticated user."""
24
+ if hasattr(self, 'queryset') and self.queryset is not None:
25
+ return self.queryset.filter(user=self.request.user)
26
+ return super().get_queryset()
27
+
28
+ def perform_create(self, serializer):
29
+ """Automatically set user on creation."""
30
+ serializer.save(user=self.request.user)
31
+
32
+ def handle_exception(self, exc):
33
+ """Enhanced error handling with logging."""
34
+ logger.error(
35
+ f"API Error in {self.__class__.__name__}: {exc}",
36
+ extra={
37
+ 'user_id': getattr(self.request.user, 'id', None),
38
+ 'path': self.request.path,
39
+ 'method': self.request.method,
40
+ 'data': getattr(self.request, 'data', None)
41
+ }
42
+ )
43
+ return super().handle_exception(exc)
44
+
45
+ def get_service(self):
46
+ """Get service instance for this view."""
47
+ if not hasattr(self, 'service_class'):
48
+ raise NotImplementedError("ViewSet must define service_class")
49
+ return self.service_class(user=self.request.user)