django-cfg 1.1.81__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +20 -448
- django_cfg/apps/accounts/README.md +3 -3
- django_cfg/apps/accounts/admin/__init__.py +0 -2
- django_cfg/apps/accounts/admin/activity.py +2 -9
- django_cfg/apps/accounts/admin/filters.py +0 -42
- django_cfg/apps/accounts/admin/inlines.py +8 -8
- django_cfg/apps/accounts/admin/otp.py +5 -5
- django_cfg/apps/accounts/admin/registration_source.py +1 -8
- django_cfg/apps/accounts/admin/user.py +12 -20
- django_cfg/apps/accounts/managers/user_manager.py +2 -129
- django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
- django_cfg/apps/accounts/models.py +3 -123
- django_cfg/apps/accounts/serializers/otp.py +40 -44
- django_cfg/apps/accounts/serializers/profile.py +0 -2
- django_cfg/apps/accounts/services/otp_service.py +98 -186
- django_cfg/apps/accounts/signals.py +25 -15
- django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
- django_cfg/apps/accounts/views/otp.py +35 -36
- django_cfg/apps/agents/README.md +129 -0
- django_cfg/apps/agents/__init__.py +68 -0
- django_cfg/apps/agents/admin/__init__.py +17 -0
- django_cfg/apps/agents/admin/execution_admin.py +460 -0
- django_cfg/apps/agents/admin/registry_admin.py +360 -0
- django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
- django_cfg/apps/agents/apps.py +29 -0
- django_cfg/apps/agents/core/__init__.py +20 -0
- django_cfg/apps/agents/core/agent.py +281 -0
- django_cfg/apps/agents/core/dependencies.py +154 -0
- django_cfg/apps/agents/core/exceptions.py +66 -0
- django_cfg/apps/agents/core/models.py +106 -0
- django_cfg/apps/agents/core/orchestrator.py +391 -0
- django_cfg/apps/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- django_cfg/apps/agents/integration/__init__.py +14 -0
- django_cfg/apps/agents/integration/middleware.py +80 -0
- django_cfg/apps/agents/integration/registry.py +345 -0
- django_cfg/apps/agents/integration/signals.py +50 -0
- django_cfg/apps/agents/management/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/create_agent.py +365 -0
- django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
- django_cfg/apps/agents/managers/__init__.py +23 -0
- django_cfg/apps/agents/managers/execution.py +236 -0
- django_cfg/apps/agents/managers/registry.py +254 -0
- django_cfg/apps/agents/managers/toolsets.py +496 -0
- django_cfg/apps/agents/migrations/0001_initial.py +286 -0
- django_cfg/apps/agents/migrations/__init__.py +5 -0
- django_cfg/apps/agents/models/__init__.py +15 -0
- django_cfg/apps/agents/models/execution.py +215 -0
- django_cfg/apps/agents/models/registry.py +220 -0
- django_cfg/apps/agents/models/toolsets.py +305 -0
- django_cfg/apps/agents/patterns/__init__.py +24 -0
- django_cfg/apps/agents/patterns/content_agents.py +234 -0
- django_cfg/apps/agents/toolsets/__init__.py +15 -0
- django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
- django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
- django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
- django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
- django_cfg/apps/agents/urls.py +46 -0
- django_cfg/apps/knowbase/README.md +150 -0
- django_cfg/apps/knowbase/__init__.py +27 -0
- django_cfg/apps/knowbase/admin/__init__.py +23 -0
- django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
- django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
- django_cfg/apps/knowbase/admin/document_admin.py +650 -0
- django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
- django_cfg/apps/knowbase/apps.py +81 -0
- django_cfg/apps/knowbase/config/README.md +176 -0
- django_cfg/apps/knowbase/config/__init__.py +51 -0
- django_cfg/apps/knowbase/config/constance_fields.py +186 -0
- django_cfg/apps/knowbase/config/constance_settings.py +200 -0
- django_cfg/apps/knowbase/config/settings.py +444 -0
- django_cfg/apps/knowbase/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- django_cfg/apps/knowbase/management/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
- django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
- django_cfg/apps/knowbase/managers/__init__.py +22 -0
- django_cfg/apps/knowbase/managers/archive.py +426 -0
- django_cfg/apps/knowbase/managers/base.py +32 -0
- django_cfg/apps/knowbase/managers/chat.py +141 -0
- django_cfg/apps/knowbase/managers/document.py +203 -0
- django_cfg/apps/knowbase/managers/external_data.py +471 -0
- django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
- django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
- django_cfg/apps/knowbase/migrations/__init__.py +5 -0
- django_cfg/apps/knowbase/mixins/__init__.py +15 -0
- django_cfg/apps/knowbase/mixins/config.py +108 -0
- django_cfg/apps/knowbase/mixins/creator.py +81 -0
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
- django_cfg/apps/knowbase/mixins/service.py +362 -0
- django_cfg/apps/knowbase/models/__init__.py +41 -0
- django_cfg/apps/knowbase/models/archive.py +599 -0
- django_cfg/apps/knowbase/models/base.py +58 -0
- django_cfg/apps/knowbase/models/chat.py +157 -0
- django_cfg/apps/knowbase/models/document.py +267 -0
- django_cfg/apps/knowbase/models/external_data.py +376 -0
- django_cfg/apps/knowbase/serializers/__init__.py +68 -0
- django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
- django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
- django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
- django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
- django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
- django_cfg/apps/knowbase/services/__init__.py +40 -0
- django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
- django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
- django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
- django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
- django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
- django_cfg/apps/knowbase/services/base.py +53 -0
- django_cfg/apps/knowbase/services/chat_service.py +239 -0
- django_cfg/apps/knowbase/services/document_service.py +144 -0
- django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
- django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
- django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
- django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
- django_cfg/apps/knowbase/services/embedding/models.py +229 -0
- django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
- django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
- django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
- django_cfg/apps/knowbase/services/search_service.py +293 -0
- django_cfg/apps/knowbase/signals/__init__.py +21 -0
- django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
- django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
- django_cfg/apps/knowbase/signals/document_signals.py +143 -0
- django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
- django_cfg/apps/knowbase/tasks/__init__.py +39 -0
- django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
- django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
- django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
- django_cfg/apps/knowbase/urls.py +43 -0
- django_cfg/apps/knowbase/utils/__init__.py +12 -0
- django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
- django_cfg/apps/knowbase/utils/text_processing.py +375 -0
- django_cfg/apps/knowbase/utils/validation.py +99 -0
- django_cfg/apps/knowbase/views/__init__.py +28 -0
- django_cfg/apps/knowbase/views/archive_views.py +469 -0
- django_cfg/apps/knowbase/views/base.py +49 -0
- django_cfg/apps/knowbase/views/chat_views.py +181 -0
- django_cfg/apps/knowbase/views/document_views.py +183 -0
- django_cfg/apps/knowbase/views/public_views.py +129 -0
- django_cfg/apps/leads/admin.py +70 -0
- django_cfg/apps/newsletter/admin.py +234 -0
- django_cfg/apps/newsletter/admin_filters.py +124 -0
- django_cfg/apps/support/admin.py +196 -0
- django_cfg/apps/support/admin_filters.py +71 -0
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +5 -4
- django_cfg/cli/README.md +1 -1
- django_cfg/cli/commands/create_project.py +2 -2
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/config.py +44 -0
- django_cfg/core/config.py +29 -82
- django_cfg/core/environment.py +1 -1
- django_cfg/core/generation.py +19 -107
- django_cfg/{integration.py → core/integration.py} +18 -16
- django_cfg/core/validation.py +1 -1
- django_cfg/management/__init__.py +1 -1
- django_cfg/management/commands/__init__.py +1 -1
- django_cfg/management/commands/auto_generate.py +482 -0
- django_cfg/management/commands/migrator.py +19 -101
- django_cfg/management/commands/test_email.py +1 -1
- django_cfg/middleware/README.md +0 -158
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/api.py +145 -0
- django_cfg/models/base.py +287 -0
- django_cfg/models/cache.py +4 -4
- django_cfg/models/constance.py +25 -88
- django_cfg/models/database.py +9 -9
- django_cfg/models/drf.py +3 -36
- django_cfg/models/email.py +163 -0
- django_cfg/models/environment.py +276 -0
- django_cfg/models/limits.py +1 -1
- django_cfg/models/logging.py +366 -0
- django_cfg/models/revolution.py +41 -2
- django_cfg/models/security.py +125 -0
- django_cfg/models/services.py +1 -1
- django_cfg/modules/__init__.py +2 -56
- django_cfg/modules/base.py +78 -52
- django_cfg/modules/django_currency/service.py +2 -2
- django_cfg/modules/django_email.py +2 -2
- django_cfg/modules/django_health.py +267 -0
- django_cfg/modules/django_llm/llm/client.py +79 -17
- django_cfg/modules/django_llm/translator/translator.py +2 -2
- django_cfg/modules/django_logger.py +2 -2
- django_cfg/modules/django_ngrok.py +2 -2
- django_cfg/modules/django_tasks.py +68 -3
- django_cfg/modules/django_telegram.py +3 -3
- django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
- django_cfg/modules/django_twilio/service.py +2 -2
- django_cfg/modules/django_twilio/simple_service.py +2 -2
- django_cfg/modules/django_twilio/templates/guide.md +266 -0
- django_cfg/modules/django_twilio/twilio_service.py +2 -2
- django_cfg/modules/django_unfold/__init__.py +69 -0
- django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
- django_cfg/modules/django_unfold/dashboard.py +278 -0
- django_cfg/modules/django_unfold/icons/README.md +145 -0
- django_cfg/modules/django_unfold/icons/__init__.py +12 -0
- django_cfg/modules/django_unfold/icons/constants.py +2851 -0
- django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
- django_cfg/modules/django_unfold/models/__init__.py +42 -0
- django_cfg/modules/django_unfold/models/config.py +601 -0
- django_cfg/modules/django_unfold/models/dashboard.py +206 -0
- django_cfg/modules/django_unfold/models/dropdown.py +40 -0
- django_cfg/modules/django_unfold/models/navigation.py +73 -0
- django_cfg/modules/django_unfold/models/tabs.py +25 -0
- django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
- django_cfg/modules/django_unfold/utils.py +140 -0
- django_cfg/registry/__init__.py +23 -0
- django_cfg/registry/core.py +61 -0
- django_cfg/registry/exceptions.py +11 -0
- django_cfg/registry/modules.py +12 -0
- django_cfg/registry/services.py +26 -0
- django_cfg/registry/third_party.py +52 -0
- django_cfg/routing/__init__.py +19 -0
- django_cfg/routing/callbacks.py +198 -0
- django_cfg/routing/routers.py +48 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
- django_cfg/templatetags/__init__.py +0 -0
- django_cfg/templatetags/django_cfg.py +33 -0
- django_cfg/urls.py +33 -0
- django_cfg/utils/path_resolution.py +1 -1
- django_cfg/utils/smart_defaults.py +7 -61
- django_cfg/utils/toolkit.py +663 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
- django_cfg-1.2.0.dist-info/RECORD +441 -0
- django_cfg/apps/tasks/@docs/README.md +0 -195
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/models/unfold.py +0 -271
- django_cfg/modules/unfold/__init__.py +0 -29
- django_cfg/modules/unfold/dashboard.py +0 -318
- django_cfg/pyproject.toml +0 -370
- django_cfg/routers.py +0 -83
- django_cfg-1.1.81.dist-info/RECORD +0 -278
- /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
- /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
- /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
- /django_cfg/{version_check.py → utils/version_check.py} +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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)
|