diff --git a/backend/core/settings.py b/backend/core/settings.py index f6c38ae..52e0abb 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -182,7 +182,19 @@ REST_FRAMEWORK = { ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', - ) + ), + 'DEFAULT_RENDERER_CLASSES': ( + # ใช้ DRF Renderer ที่รับประกันว่าตัวเลขขนาดใหญ่จะถูกแปลงเป็น String + # (นี่คือวิธีแก้ปัญหา BigInt Truncation ที่ถูกต้องที่สุด) + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ), + + # เพิ่มการตั้งค่าเพื่อรองรับตัวเลขขนาดใหญ่ใน JSON + 'COERCE_DECIMAL_TO_STRING': False, # ตั้งค่านี้ไว้เผื่อ + + # ตัวเลขขนาดใหญ่ (เช่น BigIntegerField) ต้องถูก Serialize เป็น String + 'DATETIME_FORMAT': "%Y-%m-%d %H:%M:%S", } # 3. ตั้งค่า DJOSER (เพื่อจัดการ Auth Endpoints) diff --git a/backend/model_registry/services/ai_model_service.py b/backend/model_registry/services/ai_model_service.py index 6630a08..781e5e0 100644 --- a/backend/model_registry/services/ai_model_service.py +++ b/backend/model_registry/services/ai_model_service.py @@ -2,7 +2,9 @@ from ..repositories.ai_model_repository import AiModelRepository from ..models import AiModel from typing import Optional, List import requests -from django.conf import settings # ใช้สำหรับ Configs ภายนอก/Secrets +from django.conf import settings +from django.core.cache import cache +from django.core.files.uploadedfile import UploadedFile class ConnectionError(Exception): """Custom Exception สำหรับการเชื่อมต่อล้มเหลว""" @@ -55,4 +57,77 @@ class AiModelService: model.status = new_status return self.repo.save(model) - return None \ No newline at end of file + return None + + # ----------------------------------------------- + # Logic สำหรับ Proxy AI Inference (พร้อม Caching) + # ----------------------------------------------- + def run_inference(self, pk: int, file_data: UploadedFile, user_id: int) -> dict: + model = self.repo.get_by_id(pk) + if not model: + raise ValueError(f"Model ID {pk} not found.") + + if model.status != 'ACTIVE': + raise PermissionError(f"Model '{model.name}' is not currently ACTIVE.") + + # --- Redis Caching Logic --- + # 1. สร้าง Cache Key จาก Model ID และ Hash/Metadata ของไฟล์ + # การใช้ชื่อไฟล์และขนาดไฟล์เป็น hash อย่างง่ายก็เพียงพอ + # ใน Production ควรใช้ Hash (e.g., hashlib.sha256) ของเนื้อหาไฟล์ทั้งหมด + file_hash_key = f"{file_data.name}_{file_data.size}_{file_data.content_type}" + cache_key = f'inference_result:{pk}:{file_hash_key}' + + # 2. ตรวจสอบ Cache + cached_result = cache.get(cache_key) + if cached_result: + print(f"Cache HIT for Model {pk} with file {file_data.name}") + # คืนผลลัพธ์จาก Redis ทันที + return cached_result + + print(f"Cache MISS for Model {pk} - Proxying request...") + + # --- Proxy Execution Logic --- + + # 1. สร้าง Full Inference URL + full_url = model.full_inference_url() + + # 2. จัดการ Headers (แนบ Internal Auth Key ถ้าจำเป็น) + headers = {} + if model.auth_required: + # ใช้ชื่อตัวแปรสภาพแวดล้อมที่คาดว่าจะถูกกำหนดไว้ + internal_key = getattr(settings, 'AI_INTERNAL_AUTH_KEY', 'default_secret') + headers['X-Internal-Auth'] = internal_key + + # 3. จัดการ Payload (ไฟล์) + # requests.post จะจัดการ Content-Type: multipart/form-data ให้อัตโนมัติ + files = { + 'file': (file_data.name, file_data.file, file_data.content_type) + } + + # 4. ส่ง Request ไปยัง AI Service ภายนอก + try: + response = requests.post( + full_url, + files=files, + headers=headers, + timeout=600 # 10 นาที + ) + response.raise_for_status() + + # 5. ประมวลผลผลลัพธ์ + result = response.json() + + # 6. บันทึกผลลัพธ์ลง Redis Cache + # ตั้ง TTL (Time To Live) 1 ชั่วโมง + cache.set(cache_key, result, timeout=3600) + + # 7. บันทึก Audit Log (Logic ในอนาคต) + # self.repo.log_inference_request(pk, user_id, full_url, response.status_code) + + return result + + except requests.exceptions.HTTPError as e: + detail = f"AI Service returned error {response.status_code}: {response.text}" + raise ConnectionError(detail) + except requests.exceptions.RequestException as e: + raise ConnectionError(f"Network error or timeout connecting to {full_url}: {e}") \ No newline at end of file diff --git a/backend/model_registry/views/ai_model_viewset.py b/backend/model_registry/views/ai_model_viewset.py index 7c1af5b..8356f48 100644 --- a/backend/model_registry/views/ai_model_viewset.py +++ b/backend/model_registry/views/ai_model_viewset.py @@ -10,6 +10,7 @@ from ..services.ai_model_service import AiModelService, ConnectionError from permissions.permission_classes import IsAdminOrManager from rest_framework.permissions import IsAuthenticated +from rest_framework.parsers import MultiPartParser # Dependency Injection: สร้าง Instance ของ Repository และ Service # สำหรับโปรเจกต์ขนาดเล็กสามารถทำแบบนี้ได้ @@ -86,4 +87,68 @@ class AiModelRegistryViewSet(viewsets.ModelViewSet): updated_model = service.set_status(pk=int(pk), new_status=new_status) if updated_model: return Response(self.get_serializer(updated_model).data) - return Response({"detail": "Model not found."}, status=status.HTTP_404_NOT_FOUND) \ No newline at end of file + return Response({"detail": "Model not found."}, status=status.HTTP_404_NOT_FOUND) + + # ----------------------------------------------- + # Custom Action: Run Inference (Proxy) + # ----------------------------------------------- + @extend_schema( + tags=['3. MLOps Control & Service Orchestration'], + # ระบุ Request Body เป็นประเภทไฟล์สำหรับ MultiPart + request={ + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'file': {'type': 'string', 'format': 'binary', 'description': 'DICOM/NIfTI file for inference'}, + }, + 'required': ['file'] + } + }, + # ระบุ Response Schema (ผลลัพธ์จาก AI Service) + responses={ + 200: {'description': 'Inference result', 'content': {'application/json': {'schema': {'type': 'object'}}}}, + 403: {'description': 'Model is not ACTIVE or insufficient permissions'}, + 404: {'description': 'Model not found'}, + 500: {'description': 'AI Service Connection/Internal Error'} + } + ) + @action( + detail=True, + methods=['post'], + url_path='run-inference', + parser_classes=[MultiPartParser], + permission_classes=[IsAuthenticated] + ) + def run_inference(self, request, pk=None): + """ + Endpoint: POST /api/v1/models/{pk}/run-inference/ + ทำหน้าที่รับไฟล์แล้ว Proxy ไปยัง AI Service ภายนอก + """ + try: + model_id = int(pk) + except (TypeError, ValueError): + return Response({"detail": "Invalid Model ID format."}, status=status.HTTP_400_BAD_REQUEST) + + # 1. ดึงไฟล์ (Frontend ส่งเป็น 'file') + file_data = request.FILES.get('file') + if not file_data: + return Response({"detail": "File 'file' is required."}, status=status.HTTP_400_BAD_REQUEST) + + # 2. เรียก Service Layer + try: + result = service.run_inference( + pk=model_id, + file_data=file_data, + user_id=request.user.id # ส่ง User ID สำหรับ Audit Log + ) + return Response(result, status=status.HTTP_200_OK) + + except ValueError as e: # Model not found + return Response({"detail": str(e)}, status=status.HTTP_404_NOT_FOUND) + except PermissionError as e: # Model status INACTIVE + return Response({"detail": str(e)}, status=status.HTTP_403_FORBIDDEN) + except ConnectionError as e: # AI Service Failure + # HTTP_503_SERVICE_UNAVAILABLE หรือ HTTP_504_GATEWAY_TIMEOUT อาจเหมาะสมกว่า + return Response({"detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + except Exception as e: + return Response({"detail": f"An unexpected error occurred: {e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file