พัฒนา Backend (เพิ่ม Logic การรัน Inference ใน Service Layer)
This commit is contained in:
parent
2eba6a099e
commit
16dd285bab
@ -182,7 +182,19 @@ REST_FRAMEWORK = {
|
|||||||
),
|
),
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
'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)
|
# 3. ตั้งค่า DJOSER (เพื่อจัดการ Auth Endpoints)
|
||||||
|
|||||||
@ -2,7 +2,9 @@ from ..repositories.ai_model_repository import AiModelRepository
|
|||||||
from ..models import AiModel
|
from ..models import AiModel
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
import requests
|
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):
|
class ConnectionError(Exception):
|
||||||
"""Custom Exception สำหรับการเชื่อมต่อล้มเหลว"""
|
"""Custom Exception สำหรับการเชื่อมต่อล้มเหลว"""
|
||||||
@ -55,4 +57,77 @@ class AiModelService:
|
|||||||
|
|
||||||
model.status = new_status
|
model.status = new_status
|
||||||
return self.repo.save(model)
|
return self.repo.save(model)
|
||||||
return None
|
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}")
|
||||||
@ -10,6 +10,7 @@ from ..services.ai_model_service import AiModelService, ConnectionError
|
|||||||
|
|
||||||
from permissions.permission_classes import IsAdminOrManager
|
from permissions.permission_classes import IsAdminOrManager
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser
|
||||||
|
|
||||||
# Dependency Injection: สร้าง Instance ของ Repository และ Service
|
# 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)
|
updated_model = service.set_status(pk=int(pk), new_status=new_status)
|
||||||
if updated_model:
|
if updated_model:
|
||||||
return Response(self.get_serializer(updated_model).data)
|
return Response(self.get_serializer(updated_model).data)
|
||||||
return Response({"detail": "Model not found."}, status=status.HTTP_404_NOT_FOUND)
|
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)
|
||||||
Loading…
x
Reference in New Issue
Block a user