Spaces:
Sleeping
Sleeping
Commit ·
05ec3fd
1
Parent(s): f03da0e
Solve that OutputNode part
Browse files- backend/.DS_Store +0 -0
- backend/.gitignore +4 -38
- backend/api/__pycache__/middleware.cpython-313.pyc +0 -0
- backend/api/__pycache__/urls.cpython-313.pyc +0 -0
- backend/api/__pycache__/views.cpython-313.pyc +0 -0
- backend/api/middleware.py +19 -0
- backend/api/urls.py +2 -1
- backend/api/views.py +165 -56
- backend/core/__pycache__/settings.cpython-313.pyc +0 -0
- backend/core/__pycache__/urls.cpython-313.pyc +0 -0
- backend/core/settings.py +21 -0
- backend/db.sqlite3 +2 -2
- frontend/src/App.js +18 -7
- frontend/src/CustomNode.js +2 -2
- frontend/src/api.js +39 -0
backend/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
backend/.gitignore
CHANGED
|
@@ -1,39 +1,5 @@
|
|
| 1 |
-
# Python
|
| 2 |
-
__pycache__/
|
| 3 |
-
*.pyc
|
| 4 |
-
*.pyo
|
| 5 |
-
*.pyd
|
| 6 |
-
*build/
|
| 7 |
-
*develop-eggs/
|
| 8 |
-
*dist/
|
| 9 |
-
*downloads/
|
| 10 |
-
*eggs/
|
| 11 |
-
*.egg-info/
|
| 12 |
-
*lib/
|
| 13 |
-
*lib64/
|
| 14 |
-
*parts/
|
| 15 |
-
*sdist/
|
| 16 |
-
*var/
|
| 17 |
-
*wheels/
|
| 18 |
-
*share/python-wheels/
|
| 19 |
-
*.egg-info/
|
| 20 |
-
*pip-wheel-metadata/
|
| 21 |
-
*share/jupyter/
|
| 22 |
-
*profile_default/
|
| 23 |
-
*ipython_config.py
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
.venv/
|
| 30 |
-
.env/
|
| 31 |
-
|
| 32 |
-
# Database
|
| 33 |
-
db.sqlite3
|
| 34 |
-
*.sqlite3
|
| 35 |
-
|
| 36 |
-
# Django
|
| 37 |
-
*.log
|
| 38 |
-
local_settings.py
|
| 39 |
-
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
# User-generated files
|
| 3 |
+
/media/
|
| 4 |
+
/uploads/
|
| 5 |
+
/gradio_output/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/api/__pycache__/middleware.cpython-313.pyc
ADDED
|
Binary file (1.34 kB). View file
|
|
|
backend/api/__pycache__/urls.cpython-313.pyc
CHANGED
|
Binary files a/backend/api/__pycache__/urls.cpython-313.pyc and b/backend/api/__pycache__/urls.cpython-313.pyc differ
|
|
|
backend/api/__pycache__/views.cpython-313.pyc
CHANGED
|
Binary files a/backend/api/__pycache__/views.cpython-313.pyc and b/backend/api/__pycache__/views.cpython-313.pyc differ
|
|
|
backend/api/middleware.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib.auth.models import User
|
| 2 |
+
from django.contrib.auth import login
|
| 3 |
+
|
| 4 |
+
class AutoUserCreationMiddleware:
|
| 5 |
+
def __init__(self, get_response):
|
| 6 |
+
self.get_response = get_response
|
| 7 |
+
|
| 8 |
+
def __call__(self, request):
|
| 9 |
+
if not request.user.is_authenticated:
|
| 10 |
+
# Create a new user with a unique username
|
| 11 |
+
username = f"user_{User.objects.count() + 1}"
|
| 12 |
+
user = User.objects.create_user(username=username)
|
| 13 |
+
user.save()
|
| 14 |
+
|
| 15 |
+
# Log the user in to establish a session
|
| 16 |
+
login(request, user)
|
| 17 |
+
|
| 18 |
+
response = self.get_response(request)
|
| 19 |
+
return response
|
backend/api/urls.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
from django.urls import path
|
| 2 |
-
from .views import GradioView, PredictView, ResultView, FileUploadView
|
| 3 |
|
| 4 |
urlpatterns = [
|
| 5 |
path('space/', GradioView.as_view(), name='get_space_details'),
|
| 6 |
path('predict/', PredictView.as_view(), name='predict_endpoint'),
|
| 7 |
path('result/<str:job_id>/', ResultView.as_view(), name='get_result'),
|
| 8 |
path('upload/', FileUploadView.as_view(), name='file_upload'),
|
|
|
|
| 9 |
]
|
|
|
|
| 1 |
from django.urls import path
|
| 2 |
+
from .views import GradioView, PredictView, ResultView, FileUploadView, BackendLogView
|
| 3 |
|
| 4 |
urlpatterns = [
|
| 5 |
path('space/', GradioView.as_view(), name='get_space_details'),
|
| 6 |
path('predict/', PredictView.as_view(), name='predict_endpoint'),
|
| 7 |
path('result/<str:job_id>/', ResultView.as_view(), name='get_result'),
|
| 8 |
path('upload/', FileUploadView.as_view(), name='file_upload'),
|
| 9 |
+
path('logs/', BackendLogView.as_view(), name='get_backend_logs'),
|
| 10 |
]
|
backend/api/views.py
CHANGED
|
@@ -2,135 +2,244 @@ import logging
|
|
| 2 |
import uuid
|
| 3 |
import os
|
| 4 |
import shutil
|
|
|
|
| 5 |
from urllib.parse import urlparse
|
| 6 |
from django.conf import settings
|
| 7 |
from django.core.files.storage import FileSystemStorage
|
| 8 |
from rest_framework.views import APIView
|
| 9 |
from rest_framework.response import Response
|
| 10 |
-
from rest_framework import status
|
| 11 |
from .gradio_helpers import get_space_details
|
| 12 |
from gradio_client import Client, exceptions
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 15 |
|
| 16 |
JOBS = {}
|
| 17 |
CLIENT_CACHE = {}
|
| 18 |
|
| 19 |
class FileUploadView(APIView):
|
|
|
|
|
|
|
| 20 |
def post(self, request, *args, **kwargs):
|
|
|
|
| 21 |
file_obj = request.FILES.get('file')
|
| 22 |
if not file_obj:
|
|
|
|
| 23 |
return Response({"error": "File not provided"}, status=status.HTTP_400_BAD_REQUEST)
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
filename = fs.save(file_obj.name, file_obj)
|
| 26 |
file_path = fs.path(filename)
|
|
|
|
|
|
|
| 27 |
return Response({"path": file_path}, status=status.HTTP_201_CREATED)
|
| 28 |
|
| 29 |
class GradioView(APIView):
|
|
|
|
|
|
|
| 30 |
def get(self, request, *args, **kwargs):
|
|
|
|
| 31 |
space_id = request.query_params.get('space_id')
|
| 32 |
-
|
| 33 |
if not space_id:
|
| 34 |
-
|
| 35 |
return Response({"error": "space_id is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
| 36 |
if space_id.startswith('http'):
|
|
|
|
| 37 |
try:
|
| 38 |
parsed_url = urlparse(space_id)
|
| 39 |
path_parts = parsed_url.path.strip('/').split('/')
|
| 40 |
if len(path_parts) >= 2 and path_parts[0] == 'spaces':
|
| 41 |
extracted_id = f"{path_parts[1]}/{path_parts[2]}"
|
| 42 |
-
|
| 43 |
space_id = extracted_id
|
| 44 |
except Exception as e:
|
| 45 |
-
|
| 46 |
return Response({"error": "Invalid Hugging Face Space URL provided."}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
| 47 |
try:
|
| 48 |
-
|
| 49 |
details = get_space_details(space_id)
|
| 50 |
-
|
| 51 |
return Response(details)
|
| 52 |
except Exception as e:
|
| 53 |
error_str = str(e)
|
| 54 |
-
|
| 55 |
-
if "argument of type 'bool' is not iterable" in error_str or "Could not fetch config" in error_str:
|
| 56 |
-
error_message = "The remote Hugging Face space is either invalid or has an incompatible API configuration. Please try a different space."
|
| 57 |
-
logging.warning(f"Incompatible API for space '{space_id}': {error_message}")
|
| 58 |
-
return Response({"error": error_message}, status=status.HTTP_502_BAD_GATEWAY)
|
| 59 |
return Response({"error": error_str}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
| 60 |
|
| 61 |
class PredictView(APIView):
|
|
|
|
|
|
|
| 62 |
def post(self, request, *args, **kwargs):
|
|
|
|
|
|
|
| 63 |
space_id = request.data.get('space_id')
|
| 64 |
api_name = request.data.get('api_name')
|
| 65 |
inputs = request.data.get('inputs', [])
|
| 66 |
-
|
| 67 |
-
|
| 68 |
if not all([space_id, api_name]):
|
| 69 |
-
|
| 70 |
return Response({"error": "space_id and api_name are required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
| 71 |
try:
|
| 72 |
if space_id in CLIENT_CACHE:
|
| 73 |
-
|
| 74 |
client = CLIENT_CACHE[space_id]
|
| 75 |
else:
|
| 76 |
-
|
| 77 |
client = Client(space_id)
|
| 78 |
CLIENT_CACHE[space_id] = client
|
| 79 |
-
|
|
|
|
|
|
|
| 80 |
job = client.submit(*inputs, api_name=api_name)
|
| 81 |
job_id = str(uuid.uuid4())
|
| 82 |
-
JOBS[job_id] = job
|
| 83 |
-
|
| 84 |
return Response({"job_id": job_id}, status=status.HTTP_202_ACCEPTED)
|
| 85 |
except Exception as e:
|
| 86 |
-
|
| 87 |
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
| 88 |
|
| 89 |
class ResultView(APIView):
|
|
|
|
|
|
|
| 90 |
def _process_result(self, result, request):
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
return [self._process_result(item, request) for item in result]
|
| 93 |
-
|
|
|
|
|
|
|
| 94 |
try:
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
return url
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
except Exception as e:
|
| 102 |
-
|
|
|
|
| 103 |
return result
|
|
|
|
|
|
|
| 104 |
return result
|
| 105 |
|
| 106 |
def get(self, request, job_id, *args, **kwargs):
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
if
|
| 110 |
-
|
| 111 |
return Response({"error": "Job not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
try:
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
except Exception as e:
|
| 127 |
-
|
| 128 |
JOBS.pop(job_id, None)
|
| 129 |
-
|
| 130 |
-
status_name = job.status().code.name.lower()
|
| 131 |
-
if status_name in ("cancelled", "canceled"):
|
| 132 |
-
logging.info(f"Job {job_id} was cancelled.")
|
| 133 |
-
return Response({"status": "cancelled"}, status=status.HTTP_200_OK)
|
| 134 |
-
except Exception:
|
| 135 |
-
pass
|
| 136 |
-
return Response({"status": "error", "error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
| 2 |
import uuid
|
| 3 |
import os
|
| 4 |
import shutil
|
| 5 |
+
import pathlib
|
| 6 |
from urllib.parse import urlparse
|
| 7 |
from django.conf import settings
|
| 8 |
from django.core.files.storage import FileSystemStorage
|
| 9 |
from rest_framework.views import APIView
|
| 10 |
from rest_framework.response import Response
|
| 11 |
+
from rest_framework import status, permissions
|
| 12 |
from .gradio_helpers import get_space_details
|
| 13 |
from gradio_client import Client, exceptions
|
| 14 |
|
| 15 |
+
# In-memory log storage for debugging
|
| 16 |
+
BACKEND_LOGS = []
|
| 17 |
+
|
| 18 |
+
def log_backend_message(message):
|
| 19 |
+
"""Adds a message to the in-memory log store."""
|
| 20 |
+
logging.info(message)
|
| 21 |
+
BACKEND_LOGS.append(f"INFO: {message}")
|
| 22 |
+
|
| 23 |
+
def log_backend_error(message, exc_info=False):
|
| 24 |
+
"""Adds an error to the in-memory log store."""
|
| 25 |
+
logging.error(message, exc_info=exc_info)
|
| 26 |
+
BACKEND_LOGS.append(f"ERROR: {message}")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class BackendLogView(APIView):
|
| 30 |
+
"""An endpoint to fetch and clear backend logs for debugging."""
|
| 31 |
+
permission_classes = [permissions.IsAuthenticated]
|
| 32 |
+
|
| 33 |
+
def get(self, request, *args, **kwargs):
|
| 34 |
+
logs = BACKEND_LOGS.copy()
|
| 35 |
+
BACKEND_LOGS.clear()
|
| 36 |
+
return Response({"logs": logs})
|
| 37 |
+
|
| 38 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 39 |
|
| 40 |
JOBS = {}
|
| 41 |
CLIENT_CACHE = {}
|
| 42 |
|
| 43 |
class FileUploadView(APIView):
|
| 44 |
+
permission_classes = [permissions.IsAuthenticated]
|
| 45 |
+
|
| 46 |
def post(self, request, *args, **kwargs):
|
| 47 |
+
log_backend_message("FileUploadView: Received file upload request.")
|
| 48 |
file_obj = request.FILES.get('file')
|
| 49 |
if not file_obj:
|
| 50 |
+
log_backend_error("FileUploadView: No file provided in the request.")
|
| 51 |
return Response({"error": "File not provided"}, status=status.HTTP_400_BAD_REQUEST)
|
| 52 |
+
|
| 53 |
+
user_id = str(request.user.id)
|
| 54 |
+
log_backend_message(f"FileUploadView: Upload initiated by user_id: {user_id}")
|
| 55 |
+
upload_dir = os.path.join(settings.BASE_DIR, 'uploads', user_id)
|
| 56 |
+
os.makedirs(upload_dir, exist_ok=True)
|
| 57 |
+
|
| 58 |
+
fs = FileSystemStorage(location=upload_dir)
|
| 59 |
filename = fs.save(file_obj.name, file_obj)
|
| 60 |
file_path = fs.path(filename)
|
| 61 |
+
|
| 62 |
+
log_backend_message(f"FileUploadView: File '{filename}' saved to '{file_path}'.")
|
| 63 |
return Response({"path": file_path}, status=status.HTTP_201_CREATED)
|
| 64 |
|
| 65 |
class GradioView(APIView):
|
| 66 |
+
permission_classes = [permissions.IsAuthenticated]
|
| 67 |
+
|
| 68 |
def get(self, request, *args, **kwargs):
|
| 69 |
+
log_backend_message("GradioView: Received request to fetch space details.")
|
| 70 |
space_id = request.query_params.get('space_id')
|
| 71 |
+
log_backend_message(f"GradioView: Requested space_id: '{space_id}'")
|
| 72 |
if not space_id:
|
| 73 |
+
log_backend_error("GradioView: space_id parameter is missing.")
|
| 74 |
return Response({"error": "space_id is required"}, status=status.HTTP_400_BAD_REQUEST)
|
| 75 |
+
|
| 76 |
if space_id.startswith('http'):
|
| 77 |
+
log_backend_message("GradioView: space_id is a URL, attempting to parse.")
|
| 78 |
try:
|
| 79 |
parsed_url = urlparse(space_id)
|
| 80 |
path_parts = parsed_url.path.strip('/').split('/')
|
| 81 |
if len(path_parts) >= 2 and path_parts[0] == 'spaces':
|
| 82 |
extracted_id = f"{path_parts[1]}/{path_parts[2]}"
|
| 83 |
+
log_backend_message(f"GradioView: Extracted space_id '{extracted_id}' from URL.")
|
| 84 |
space_id = extracted_id
|
| 85 |
except Exception as e:
|
| 86 |
+
log_backend_error(f"GradioView: Failed to parse URL: {e}")
|
| 87 |
return Response({"error": "Invalid Hugging Face Space URL provided."}, status=status.HTTP_400_BAD_REQUEST)
|
| 88 |
+
|
| 89 |
try:
|
| 90 |
+
log_backend_message(f"GradioView: Calling get_space_details for '{space_id}'.")
|
| 91 |
details = get_space_details(space_id)
|
| 92 |
+
log_backend_message(f"GradioView: Successfully fetched details for '{space_id}'.")
|
| 93 |
return Response(details)
|
| 94 |
except Exception as e:
|
| 95 |
error_str = str(e)
|
| 96 |
+
log_backend_error(f"GradioView: Failed to get space details for '{space_id}'. Error: {error_str}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
return Response({"error": error_str}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
| 98 |
|
| 99 |
class PredictView(APIView):
|
| 100 |
+
permission_classes = [permissions.IsAuthenticated]
|
| 101 |
+
|
| 102 |
def post(self, request, *args, **kwargs):
|
| 103 |
+
BACKEND_LOGS.clear() # Clear logs for new run
|
| 104 |
+
log_backend_message("PredictView: Received new prediction request.")
|
| 105 |
space_id = request.data.get('space_id')
|
| 106 |
api_name = request.data.get('api_name')
|
| 107 |
inputs = request.data.get('inputs', [])
|
| 108 |
+
log_backend_message(f"PredictView: Space: {space_id}, API: {api_name}, Inputs: {inputs}")
|
| 109 |
+
|
| 110 |
if not all([space_id, api_name]):
|
| 111 |
+
log_backend_error("PredictView: Missing space_id or api_name.")
|
| 112 |
return Response({"error": "space_id and api_name are required"}, status=status.HTTP_400_BAD_REQUEST)
|
| 113 |
+
|
| 114 |
try:
|
| 115 |
if space_id in CLIENT_CACHE:
|
| 116 |
+
log_backend_message(f"PredictView: Reusing existing Gradio client for space: {space_id}")
|
| 117 |
client = CLIENT_CACHE[space_id]
|
| 118 |
else:
|
| 119 |
+
log_backend_message(f"PredictView: Initializing new Gradio client for space: {space_id}")
|
| 120 |
client = Client(space_id)
|
| 121 |
CLIENT_CACHE[space_id] = client
|
| 122 |
+
log_backend_message(f"PredictView: New client for {space_id} cached.")
|
| 123 |
+
|
| 124 |
+
log_backend_message("PredictView: Submitting job to Gradio client...")
|
| 125 |
job = client.submit(*inputs, api_name=api_name)
|
| 126 |
job_id = str(uuid.uuid4())
|
| 127 |
+
JOBS[job_id] = {'job': job, 'user': request.user}
|
| 128 |
+
log_backend_message(f"PredictView: Job submitted with temporary ID: {job_id}")
|
| 129 |
return Response({"job_id": job_id}, status=status.HTTP_202_ACCEPTED)
|
| 130 |
except Exception as e:
|
| 131 |
+
log_backend_error(f"PredictView: Prediction failed for space '{space_id}': {str(e)}", exc_info=True)
|
| 132 |
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
| 133 |
|
| 134 |
class ResultView(APIView):
|
| 135 |
+
permission_classes = [permissions.IsAuthenticated]
|
| 136 |
+
|
| 137 |
def _process_result(self, result, request):
|
| 138 |
+
log_backend_message(f"ResultView._process_result: Processing item. Type: {type(result)}. Value: {result}")
|
| 139 |
+
|
| 140 |
+
# Recursively process lists and tuples
|
| 141 |
+
if isinstance(result, list) or isinstance(result, tuple):
|
| 142 |
+
log_backend_message(f"ResultView._process_result: Item is a {type(result).__name__}, processing each element recursively.")
|
| 143 |
return [self._process_result(item, request) for item in result]
|
| 144 |
+
|
| 145 |
+
if isinstance(result, str):
|
| 146 |
+
# EAFP (Easier to Ask for Forgiveness than Permission) approach
|
| 147 |
try:
|
| 148 |
+
# Attempt to treat the string as a file path
|
| 149 |
+
log_backend_message(f"ResultView._process_result: Item is a string. Attempting to copy '{result}' as a file.")
|
| 150 |
+
|
| 151 |
+
# An extra check to avoid trying to copy things that are clearly not paths
|
| 152 |
+
if not os.path.sep in result:
|
| 153 |
+
log_backend_message(f"ResultView._process_result: '{result}' does not contain a path separator. Assuming it's a regular string.")
|
| 154 |
+
return result
|
| 155 |
+
|
| 156 |
+
user_id = str(request.user.id)
|
| 157 |
+
user_media_dir = os.path.join(settings.MEDIA_ROOT, user_id)
|
| 158 |
+
|
| 159 |
+
if not os.path.exists(user_media_dir):
|
| 160 |
+
os.makedirs(user_media_dir)
|
| 161 |
+
log_backend_message(f"ResultView._process_result: Created user media directory: {user_media_dir}")
|
| 162 |
+
|
| 163 |
+
# Create a unique filename to avoid conflicts
|
| 164 |
+
original_filename = os.path.basename(result)
|
| 165 |
+
unique_filename = str(uuid.uuid4()) + os.path.splitext(original_filename)[1]
|
| 166 |
+
destination_path = os.path.join(user_media_dir, unique_filename)
|
| 167 |
+
|
| 168 |
+
log_backend_message(f"ResultView._process_result: Destination path for copy: {destination_path}")
|
| 169 |
+
|
| 170 |
+
shutil.copy(result, destination_path)
|
| 171 |
+
log_backend_message("ResultView._process_result: File copied successfully.")
|
| 172 |
+
|
| 173 |
+
# Construct the public URL
|
| 174 |
+
file_url_path = f"{settings.MEDIA_URL}{user_id}/{unique_filename}"
|
| 175 |
+
url = request.build_absolute_uri(file_url_path)
|
| 176 |
+
|
| 177 |
+
log_backend_message(f"ResultView._process_result: Successfully converted file path to public URL: '{url}'")
|
| 178 |
return url
|
| 179 |
+
except (FileNotFoundError, IsADirectoryError, OSError) as e:
|
| 180 |
+
# This will trigger if `result` is not a valid file path
|
| 181 |
+
log_backend_message(f"ResultView._process_result: Could not treat '{result}' as a file. It's likely a regular string. Error: {e}")
|
| 182 |
+
return result
|
| 183 |
except Exception as e:
|
| 184 |
+
# Catch any other unexpected errors during the copy
|
| 185 |
+
log_backend_error(f"ResultView._process_result: An unexpected error occurred while processing '{result}'. Error: {e}", exc_info=True)
|
| 186 |
return result
|
| 187 |
+
|
| 188 |
+
log_backend_message(f"ResultView._process_result: Item is not a string, list, or tuple ({type(result).__name__}), returning as is.")
|
| 189 |
return result
|
| 190 |
|
| 191 |
def get(self, request, job_id, *args, **kwargs):
|
| 192 |
+
log_backend_message(f"ResultView: Received result request for job_id: {job_id}")
|
| 193 |
+
job_info = JOBS.get(job_id)
|
| 194 |
+
if job_info is None:
|
| 195 |
+
log_backend_error(f"ResultView: Job with ID {job_id} not found.")
|
| 196 |
return Response({"error": "Job not found"}, status=status.HTTP_404_NOT_FOUND)
|
| 197 |
+
|
| 198 |
+
if job_info['user'] != request.user:
|
| 199 |
+
log_backend_error(f"ResultView: User {request.user} attempted to access job {job_id} owned by {job_info['user']}")
|
| 200 |
+
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
|
| 201 |
+
|
| 202 |
+
job = job_info['job']
|
| 203 |
try:
|
| 204 |
+
status_name = job.status().code.name.lower()
|
| 205 |
+
log_backend_message(f"ResultView: Job {job_id} status is '{status_name}'.")
|
| 206 |
+
|
| 207 |
+
if status_name == "finished":
|
| 208 |
+
log_backend_message(f"ResultView: Job {job_id} is finished. Processing final result.")
|
| 209 |
+
raw_results = job.result()
|
| 210 |
+
log_backend_message(f"ResultView: Job {job_id} completed with raw result: {raw_results}")
|
| 211 |
+
|
| 212 |
+
if not isinstance(raw_results, list):
|
| 213 |
+
raw_results = [raw_results]
|
| 214 |
+
|
| 215 |
+
final_result = self._process_result(raw_results, request)
|
| 216 |
+
|
| 217 |
+
JOBS.pop(job_id, None)
|
| 218 |
+
|
| 219 |
+
# Capture the logs from this final processing step
|
| 220 |
+
final_logs = BACKEND_LOGS.copy()
|
| 221 |
+
BACKEND_LOGS.clear()
|
| 222 |
+
|
| 223 |
+
log_backend_message(f"ResultView: Final processed result for job {job_id}: {final_result}")
|
| 224 |
+
return Response({
|
| 225 |
+
"status": "completed",
|
| 226 |
+
"result": final_result,
|
| 227 |
+
"logs": final_logs # Include final logs in the response
|
| 228 |
+
}, status=status.HTTP_200_OK)
|
| 229 |
+
|
| 230 |
+
elif status_name in ["cancelled", "failed"]:
|
| 231 |
+
log_backend_message(f"ResultView: Job {job_id} ended with terminal status: {status_name}")
|
| 232 |
+
JOBS.pop(job_id, None)
|
| 233 |
+
return Response({"status": status_name, "error": f"Job ended with status: {status_name}"}, status=status.HTTP_200_OK)
|
| 234 |
+
|
| 235 |
+
else: # The job is still running
|
| 236 |
+
log_backend_message(f"ResultView: Job {job_id} still processing.")
|
| 237 |
+
# For polling requests, return the current logs
|
| 238 |
+
logs = BACKEND_LOGS.copy()
|
| 239 |
+
BACKEND_LOGS.clear()
|
| 240 |
+
return Response({"status": "processing", "detail": status_name, "logs": logs}, status=status.HTTP_200_OK)
|
| 241 |
+
|
| 242 |
except Exception as e:
|
| 243 |
+
log_backend_error(f"ResultView: Error processing job {job_id}: {e}", exc_info=True)
|
| 244 |
JOBS.pop(job_id, None)
|
| 245 |
+
return Response({"status": "error", "error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/core/__pycache__/settings.cpython-313.pyc
CHANGED
|
Binary files a/backend/core/__pycache__/settings.cpython-313.pyc and b/backend/core/__pycache__/settings.cpython-313.pyc differ
|
|
|
backend/core/__pycache__/urls.cpython-313.pyc
CHANGED
|
Binary files a/backend/core/__pycache__/urls.cpython-313.pyc and b/backend/core/__pycache__/urls.cpython-313.pyc differ
|
|
|
backend/core/settings.py
CHANGED
|
@@ -27,6 +27,10 @@ DEBUG = True
|
|
| 27 |
|
| 28 |
ALLOWED_HOSTS = []
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# Application definition
|
| 32 |
|
|
@@ -42,6 +46,22 @@ INSTALLED_APPS = [
|
|
| 42 |
'api',
|
| 43 |
]
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
MIDDLEWARE = [
|
| 46 |
'django.middleware.security.SecurityMiddleware',
|
| 47 |
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
@@ -51,6 +71,7 @@ MIDDLEWARE = [
|
|
| 51 |
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 52 |
'django.contrib.messages.middleware.MessageMiddleware',
|
| 53 |
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
|
|
| 54 |
]
|
| 55 |
|
| 56 |
ROOT_URLCONF = 'core.urls'
|
|
|
|
| 27 |
|
| 28 |
ALLOWED_HOSTS = []
|
| 29 |
|
| 30 |
+
CSRF_TRUSTED_ORIGINS = [
|
| 31 |
+
'http://localhost:3000',
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
|
| 35 |
# Application definition
|
| 36 |
|
|
|
|
| 46 |
'api',
|
| 47 |
]
|
| 48 |
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
REST_FRAMEWORK = {
|
| 52 |
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
| 53 |
+
'rest_framework.authentication.SessionAuthentication',
|
| 54 |
+
],
|
| 55 |
+
'DEFAULT_PERMISSION_CLASSES': [
|
| 56 |
+
'rest_framework.permissions.IsAuthenticated',
|
| 57 |
+
],
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
CORS_ALLOW_CREDENTIALS = True
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
MIDDLEWARE = [
|
| 66 |
'django.middleware.security.SecurityMiddleware',
|
| 67 |
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
|
|
| 71 |
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 72 |
'django.contrib.messages.middleware.MessageMiddleware',
|
| 73 |
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 74 |
+
'api.middleware.AutoUserCreationMiddleware',
|
| 75 |
]
|
| 76 |
|
| 77 |
ROOT_URLCONF = 'core.urls'
|
backend/db.sqlite3
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:61db9a29d3062089c1aac27a27f5040eb8280ded478267a97a0fbda34ba2b803
|
| 3 |
+
size 258048
|
frontend/src/App.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import React, { useState, useCallback, useMemo, useContext, createContext } from 'react';
|
| 2 |
-
import
|
| 3 |
import ReactFlow, {
|
| 4 |
MiniMap,
|
| 5 |
Controls,
|
|
@@ -124,7 +124,7 @@ function App() {
|
|
| 124 |
setNodes(nds => nds.map(n => n.id === nodeIdToReplace ? { ...n, data: { ...n.data, error: null } } : n));
|
| 125 |
|
| 126 |
try {
|
| 127 |
-
const response = await
|
| 128 |
const { endpoints } = response.data;
|
| 129 |
const priorityEndpoint = endpoints.find(e => e.category === 'priority') || endpoints[0];
|
| 130 |
|
|
@@ -159,19 +159,30 @@ function App() {
|
|
| 159 |
const node = nodesMap.get(nodeId);
|
| 160 |
addLog(`[${node.data.label}] Polling for result (Job ID: ${jobId})...`);
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
const poll = async () => {
|
| 163 |
try {
|
| 164 |
-
const response = await
|
| 165 |
|
|
|
|
|
|
|
|
|
|
| 166 |
if (response.data.status === 'processing') {
|
| 167 |
-
setTimeout(poll, 2000);
|
| 168 |
} else if (response.data.status === 'completed') {
|
| 169 |
addLog(`[${node.data.label}] Job completed successfully.`, 'SUCCESS');
|
| 170 |
executionResults.set(nodeId, response.data.result);
|
| 171 |
setNodes(nds => nds.map(n => n.id === nodeId ? { ...n, data: { ...n.data, status: 'success', result: response.data.result } } : n));
|
| 172 |
await processNextNodes(nodeId, executionResults, nodesMap);
|
| 173 |
-
} else if (response.data.status === 'error') {
|
| 174 |
-
|
| 175 |
}
|
| 176 |
} catch (e) {
|
| 177 |
addLog(`[${node.data.label}] Polling failed: ${e.message}`, 'ERROR');
|
|
@@ -234,7 +245,7 @@ function App() {
|
|
| 234 |
}).filter(input => input !== null);
|
| 235 |
|
| 236 |
addLog(`[${node.data.label}] Sending request to /api/predict/ with inputs: ${JSON.stringify(apiInputs)}`);
|
| 237 |
-
const response = await
|
| 238 |
space_id: node.data.label,
|
| 239 |
api_name: node.data.apiName,
|
| 240 |
inputs: apiInputs,
|
|
|
|
| 1 |
import React, { useState, useCallback, useMemo, useContext, createContext } from 'react';
|
| 2 |
+
import api from './api'; // Import the centralized api
|
| 3 |
import ReactFlow, {
|
| 4 |
MiniMap,
|
| 5 |
Controls,
|
|
|
|
| 124 |
setNodes(nds => nds.map(n => n.id === nodeIdToReplace ? { ...n, data: { ...n.data, error: null } } : n));
|
| 125 |
|
| 126 |
try {
|
| 127 |
+
const response = await api.get(`/space/?space_id=${spaceId}`);
|
| 128 |
const { endpoints } = response.data;
|
| 129 |
const priorityEndpoint = endpoints.find(e => e.category === 'priority') || endpoints[0];
|
| 130 |
|
|
|
|
| 159 |
const node = nodesMap.get(nodeId);
|
| 160 |
addLog(`[${node.data.label}] Polling for result (Job ID: ${jobId})...`);
|
| 161 |
|
| 162 |
+
const processLogs = (logs) => {
|
| 163 |
+
if (logs && logs.length > 0) {
|
| 164 |
+
logs.forEach(logMsg => {
|
| 165 |
+
addLog(`[BACKEND] ${logMsg}`, 'INFO');
|
| 166 |
+
});
|
| 167 |
+
}
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
const poll = async () => {
|
| 171 |
try {
|
| 172 |
+
const response = await api.get(`/result/${jobId}`);
|
| 173 |
|
| 174 |
+
// Always process logs, whether the job is running or complete
|
| 175 |
+
processLogs(response.data.logs);
|
| 176 |
+
|
| 177 |
if (response.data.status === 'processing') {
|
| 178 |
+
setTimeout(poll, 2000); // Continue polling
|
| 179 |
} else if (response.data.status === 'completed') {
|
| 180 |
addLog(`[${node.data.label}] Job completed successfully.`, 'SUCCESS');
|
| 181 |
executionResults.set(nodeId, response.data.result);
|
| 182 |
setNodes(nds => nds.map(n => n.id === nodeId ? { ...n, data: { ...n.data, status: 'success', result: response.data.result } } : n));
|
| 183 |
await processNextNodes(nodeId, executionResults, nodesMap);
|
| 184 |
+
} else if (response.data.status === 'error' || response.data.status === 'failed' || response.data.status === 'cancelled') {
|
| 185 |
+
throw new Error(response.data.error || `Job ended with status: ${response.data.status}`);
|
| 186 |
}
|
| 187 |
} catch (e) {
|
| 188 |
addLog(`[${node.data.label}] Polling failed: ${e.message}`, 'ERROR');
|
|
|
|
| 245 |
}).filter(input => input !== null);
|
| 246 |
|
| 247 |
addLog(`[${node.data.label}] Sending request to /api/predict/ with inputs: ${JSON.stringify(apiInputs)}`);
|
| 248 |
+
const response = await api.post('/predict/', {
|
| 249 |
space_id: node.data.label,
|
| 250 |
api_name: node.data.apiName,
|
| 251 |
inputs: apiInputs,
|
frontend/src/CustomNode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { Handle, Position } from 'reactflow';
|
| 3 |
-
import
|
| 4 |
import './CustomNode.css';
|
| 5 |
|
| 6 |
// A dedicated component for the slider to manage its own state
|
|
@@ -39,7 +39,7 @@ const InputRow = ({ node_id, input, isConnected, isConnectable }) => {
|
|
| 39 |
setUploadStatus('uploading');
|
| 40 |
|
| 41 |
try {
|
| 42 |
-
const response = await
|
| 43 |
headers: {
|
| 44 |
'Content-Type': 'multipart/form-data',
|
| 45 |
},
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { Handle, Position } from 'reactflow';
|
| 3 |
+
import api from './api'; // Import the centralized api
|
| 4 |
import './CustomNode.css';
|
| 5 |
|
| 6 |
// A dedicated component for the slider to manage its own state
|
|
|
|
| 39 |
setUploadStatus('uploading');
|
| 40 |
|
| 41 |
try {
|
| 42 |
+
const response = await api.post('/upload/', formData, {
|
| 43 |
headers: {
|
| 44 |
'Content-Type': 'multipart/form-data',
|
| 45 |
},
|
frontend/src/api.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from 'axios';
|
| 2 |
+
|
| 3 |
+
// Function to get the CSRF token from the cookie
|
| 4 |
+
function getCookie(name) {
|
| 5 |
+
let cookieValue = null;
|
| 6 |
+
if (document.cookie && document.cookie !== '') {
|
| 7 |
+
const cookies = document.cookie.split(';');
|
| 8 |
+
for (let i = 0; i < cookies.length; i++) {
|
| 9 |
+
const cookie = cookies[i].trim();
|
| 10 |
+
// Does this cookie string begin with the name we want?
|
| 11 |
+
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
| 12 |
+
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
| 13 |
+
break;
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
return cookieValue;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const api = axios.create({
|
| 21 |
+
baseURL: 'http://localhost:8000/api', // Your Django API base URL
|
| 22 |
+
withCredentials: true, // This is crucial for sending session cookies
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
// Add a request interceptor to include the CSRF token
|
| 26 |
+
api.interceptors.request.use(
|
| 27 |
+
(config) => {
|
| 28 |
+
const token = getCookie('csrftoken');
|
| 29 |
+
if (token) {
|
| 30 |
+
config.headers['X-CSRFToken'] = token;
|
| 31 |
+
}
|
| 32 |
+
return config;
|
| 33 |
+
},
|
| 34 |
+
(error) => {
|
| 35 |
+
return Promise.reject(error);
|
| 36 |
+
}
|
| 37 |
+
);
|
| 38 |
+
|
| 39 |
+
export default api;
|