Add health check endpoint and refactor integration response handling
- Introduced a new public health check endpoint at `api/ping/` to verify API responsiveness. - Refactored integration response handling to utilize a unified success and error response format across various methods in `IntegrationSettingsViewSet`, improving consistency and clarity in API responses. - Updated URL patterns to include the new ping endpoint and adjusted imports accordingly.
This commit is contained in:
@@ -5,7 +5,6 @@ Unified API Standard v1.0 compliant
|
||||
import logging
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
@@ -140,7 +139,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
logger.info(f"[test_connection] Testing {integration_type} connection with API key (length={len(api_key) if api_key else 0})")
|
||||
try:
|
||||
if integration_type == 'openai':
|
||||
return self._test_openai(api_key, config)
|
||||
return self._test_openai(api_key, config, request)
|
||||
elif integration_type == 'runware':
|
||||
return self._test_runware(api_key, request)
|
||||
else:
|
||||
@@ -160,7 +159,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
def _test_openai(self, api_key: str, config: dict = None):
|
||||
def _test_openai(self, api_key: str, config: dict = None, request=None):
|
||||
"""
|
||||
Test OpenAI API connection.
|
||||
EXACT match to reference plugin's igny8_test_connection() function.
|
||||
@@ -215,33 +214,38 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
rates = MODEL_RATES.get(model, {'input': 2.00, 'output': 8.00})
|
||||
cost = (input_tokens * rates['input'] + output_tokens * rates['output']) / 1000000
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'API connection and response test successful!',
|
||||
'model_used': model,
|
||||
'response': response_text,
|
||||
'tokens_used': f"{input_tokens} / {output_tokens}",
|
||||
'total_tokens': total_tokens,
|
||||
'cost': f'${cost:.4f}',
|
||||
'full_response': response_data,
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'message': 'API connection and response test successful!',
|
||||
'model_used': model,
|
||||
'response': response_text,
|
||||
'tokens_used': f"{input_tokens} / {output_tokens}",
|
||||
'total_tokens': total_tokens,
|
||||
'cost': f'${cost:.4f}',
|
||||
'full_response': response_data,
|
||||
},
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'API responded but no content received',
|
||||
'response': response.text[:500]
|
||||
})
|
||||
return error_response(
|
||||
error='API responded but no content received',
|
||||
errors={'response': [response.text[:500]]},
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
body = response.text
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': f'HTTP {response.status_code} – {body[:200]}'
|
||||
}, status=response.status_code)
|
||||
return error_response(
|
||||
error=f'HTTP {response.status_code} – {body[:200]}',
|
||||
status_code=response.status_code,
|
||||
request=request
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
# Simple connection test without API call (reference plugin: GET /v1/models)
|
||||
try:
|
||||
@@ -254,23 +258,27 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
)
|
||||
|
||||
if response.status_code >= 200 and response.status_code < 300:
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'API connection successful!',
|
||||
'model_used': model,
|
||||
'response': 'Connection verified without API call'
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'message': 'API connection successful!',
|
||||
'model_used': model,
|
||||
'response': 'Connection verified without API call'
|
||||
},
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
body = response.text
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': f'HTTP {response.status_code} – {body[:200]}'
|
||||
}, status=response.status_code)
|
||||
return error_response(
|
||||
error=f'HTTP {response.status_code} – {body[:200]}',
|
||||
status_code=response.status_code,
|
||||
request=request
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def _test_runware(self, api_key: str, request):
|
||||
"""
|
||||
@@ -338,11 +346,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
if response.status_code != 200:
|
||||
error_text = response.text
|
||||
logger.error(f"[_test_runware] HTTP error {response.status_code}: {error_text[:200]}")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'HTTP {response.status_code}: {error_text[:200]}',
|
||||
'message': 'Runware API validation failed'
|
||||
}, status=response.status_code)
|
||||
return error_response(
|
||||
error=f'HTTP {response.status_code}: {error_text[:200]}',
|
||||
status_code=response.status_code,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Parse response - Reference plugin checks: $body['data'][0]['imageURL']
|
||||
body = response.json()
|
||||
@@ -357,15 +365,17 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
image_url = first_item.get('imageURL') or first_item.get('image_url')
|
||||
if image_url:
|
||||
logger.info(f"[_test_runware] Success! Image URL: {image_url[:50]}...")
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': '✅ Runware API connected successfully!',
|
||||
'image_url': image_url,
|
||||
'cost': '$0.0090',
|
||||
'provider': 'runware',
|
||||
'model': 'runware:97@1',
|
||||
'size': '128x128'
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'message': '✅ Runware API connected successfully!',
|
||||
'image_url': image_url,
|
||||
'cost': '$0.0090',
|
||||
'provider': 'runware',
|
||||
'model': 'runware:97@1',
|
||||
'size': '128x128'
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
# Check for errors - Reference plugin line 4998: elseif (isset($body['errors'][0]['message']))
|
||||
if isinstance(body, dict) and 'errors' in body:
|
||||
@@ -373,26 +383,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
if isinstance(errors, list) and len(errors) > 0:
|
||||
error_msg = errors[0].get('message', 'Unknown Runware API error')
|
||||
logger.error(f"[_test_runware] Runware API error: {error_msg}")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'❌ {error_msg}',
|
||||
'message': 'Runware API validation failed'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=f'❌ {error_msg}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Unknown response format
|
||||
logger.error(f"[_test_runware] Unknown response format: {body}")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': '❌ Unknown response from Runware.',
|
||||
'message': 'Runware API validation failed'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error='❌ Unknown response from Runware.',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[_test_runware] Exception in Runware API test: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'Runware API test failed: {str(e)}',
|
||||
'message': 'Runware API validation failed'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=f'Runware API test failed: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def generate_image(self, request, pk=None, **kwargs):
|
||||
"""
|
||||
@@ -419,10 +429,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
if pk != 'image_generation':
|
||||
logger.error(f"[generate_image] Invalid pk: {pk}, expected 'image_generation'")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'Image generation endpoint only available for image_generation integration, got: {pk}'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Image generation endpoint only available for image_generation integration, got: {pk}',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
logger.info("[generate_image] Step 1: Getting account")
|
||||
@@ -445,10 +456,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
if not account:
|
||||
logger.error("[generate_image] ERROR: No account found, returning error response")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': 'Account not found'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Account not found',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[generate_image] Account resolved: {account.id if account else 'None'}")
|
||||
|
||||
@@ -467,10 +479,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
if not prompt:
|
||||
logger.error("[generate_image] ERROR: Prompt is empty")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': 'Prompt is required'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Prompt is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get API key from saved settings for the specified provider only
|
||||
logger.info(f"[generate_image] Step 3: Getting API key for provider: {provider}")
|
||||
@@ -502,17 +515,19 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
logger.info(f"[generate_image] Step 4: Validating {provider} provider and API key")
|
||||
if provider not in ['openai', 'runware']:
|
||||
logger.error(f"[generate_image] ERROR: Invalid provider: {provider}")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'Invalid provider: {provider}. Must be "openai" or "runware"'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Invalid provider: {provider}. Must be "openai" or "runware"',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
if not api_key or not integration_enabled:
|
||||
logger.error(f"[generate_image] ERROR: {provider.upper()} API key not configured or integration not enabled")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'{provider.upper()} API key not configured or integration not enabled'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'{provider.upper()} API key not configured or integration not enabled',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[generate_image] {provider.upper()} API key validated successfully")
|
||||
|
||||
@@ -543,14 +558,14 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
if result.get('error'):
|
||||
logger.error(f"[generate_image] ERROR from AIProcessor: {result.get('error')}")
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': result['error']
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=result['error'],
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info("[generate_image] Image generation successful, returning response")
|
||||
response_data = {
|
||||
'success': True,
|
||||
'image_url': result.get('url'),
|
||||
'revised_prompt': result.get('revised_prompt'),
|
||||
'model': model,
|
||||
@@ -558,19 +573,27 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
'cost': f"${result.get('cost', 0):.4f}" if result.get('cost') else None,
|
||||
}
|
||||
logger.info(f"[generate_image] Returning success response: {response_data}")
|
||||
return Response(response_data)
|
||||
return success_response(
|
||||
data=response_data,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[generate_image] EXCEPTION in image generation: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'success': False,
|
||||
'error': f'Failed to generate image: {str(e)}'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=f'Failed to generate image: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
"""Create integration settings"""
|
||||
integration_type = request.data.get('integration_type')
|
||||
if not integration_type:
|
||||
return Response({'error': 'integration_type is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='integration_type is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
return self.save_settings(request, integration_type)
|
||||
|
||||
def save_settings(self, request, pk=None):
|
||||
@@ -693,9 +716,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
logger.error(f"Full traceback: {error_trace}")
|
||||
return Response({
|
||||
'error': f'Failed to save settings: {str(e)}'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=f'Failed to save settings: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def get_settings(self, request, pk=None):
|
||||
"""Get integration settings - defaults to AWS-admin settings if account doesn't have its own"""
|
||||
@@ -772,10 +797,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
pass
|
||||
|
||||
if not account:
|
||||
return Response({
|
||||
'error': 'Account not found',
|
||||
'type': 'AuthenticationError'
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
return error_response(
|
||||
error='Account not found',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
try:
|
||||
from .models import IntegrationSettings
|
||||
@@ -800,39 +826,44 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
provider = config.get('provider', 'openai')
|
||||
default_featured_size = '1280x832' if provider == 'runware' else '1024x1024'
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'config': {
|
||||
'provider': config.get('provider', 'openai'),
|
||||
'model': model,
|
||||
'image_type': config.get('image_type', 'realistic'),
|
||||
'max_in_article_images': config.get('max_in_article_images', 2),
|
||||
'image_format': config.get('image_format', 'webp'),
|
||||
'desktop_enabled': config.get('desktop_enabled', True),
|
||||
'mobile_enabled': config.get('mobile_enabled', True),
|
||||
'featured_image_size': config.get('featured_image_size', default_featured_size),
|
||||
'desktop_image_size': config.get('desktop_image_size', '1024x1024'),
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
return success_response(
|
||||
data={
|
||||
'config': {
|
||||
'provider': config.get('provider', 'openai'),
|
||||
'model': model,
|
||||
'image_type': config.get('image_type', 'realistic'),
|
||||
'max_in_article_images': config.get('max_in_article_images', 2),
|
||||
'image_format': config.get('image_format', 'webp'),
|
||||
'desktop_enabled': config.get('desktop_enabled', True),
|
||||
'mobile_enabled': config.get('mobile_enabled', True),
|
||||
'featured_image_size': config.get('featured_image_size', default_featured_size),
|
||||
'desktop_image_size': config.get('desktop_image_size', '1024x1024'),
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
return Response({
|
||||
'success': True,
|
||||
'config': {
|
||||
'provider': 'openai',
|
||||
'model': 'dall-e-3',
|
||||
'image_type': 'realistic',
|
||||
'max_in_article_images': 2,
|
||||
'image_format': 'webp',
|
||||
'desktop_enabled': True,
|
||||
'mobile_enabled': True,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
return success_response(
|
||||
data={
|
||||
'config': {
|
||||
'provider': 'openai',
|
||||
'model': 'dall-e-3',
|
||||
'image_type': 'realistic',
|
||||
'max_in_article_images': 2,
|
||||
'image_format': 'webp',
|
||||
'desktop_enabled': True,
|
||||
'mobile_enabled': True,
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'error': str(e),
|
||||
'type': 'ServerError'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='task_progress/(?P<task_id>[^/.]+)', url_name='task-progress')
|
||||
def task_progress(self, request, task_id=None):
|
||||
@@ -841,9 +872,10 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
GET /api/v1/system/settings/task_progress/<task_id>/
|
||||
"""
|
||||
if not task_id:
|
||||
return Response(
|
||||
{'error': 'Task ID is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
error='Task ID is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
import logging
|
||||
@@ -862,14 +894,18 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
RedisConnectionError = ConnectionError
|
||||
except ImportError:
|
||||
logger.warning("Celery not available - task progress cannot be retrieved")
|
||||
return Response({
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Celery not available - cannot retrieve task status',
|
||||
'error': 'Celery not configured'
|
||||
}
|
||||
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Celery not available - cannot retrieve task status',
|
||||
'error': 'Celery not configured'
|
||||
}
|
||||
},
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
request=request
|
||||
)
|
||||
|
||||
try:
|
||||
# Create AsyncResult - this should not raise an exception even if task doesn't exist
|
||||
@@ -937,51 +973,64 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
except Exception as e:
|
||||
logger.debug(f"Error extracting error from task.info: {str(e)}")
|
||||
|
||||
return Response({
|
||||
'state': 'FAILURE',
|
||||
'meta': {
|
||||
'error': error_msg,
|
||||
'error_type': error_type,
|
||||
'percentage': 0,
|
||||
'message': f'Error: {error_msg}',
|
||||
'request_steps': request_steps,
|
||||
'response_steps': response_steps,
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'FAILURE',
|
||||
'meta': {
|
||||
'error': error_msg,
|
||||
'error_type': error_type,
|
||||
'percentage': 0,
|
||||
'message': f'Error: {error_msg}',
|
||||
'request_steps': request_steps,
|
||||
'response_steps': response_steps,
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except (KombuOperationalError, RedisConnectionError, ConnectionError) as conn_exc:
|
||||
# Backend connection error - task might not be registered yet or backend is down
|
||||
logger.warning(f"Backend connection error accessing task.state for {task_id}: {type(conn_exc).__name__}: {str(conn_exc)}")
|
||||
return Response({
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is being queued...',
|
||||
'phase': 'initializing',
|
||||
'error': None # Don't show as error, just pending
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is being queued...',
|
||||
'phase': 'initializing',
|
||||
'error': None # Don't show as error, just pending
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except Exception as state_exc:
|
||||
logger.error(f"Unexpected error accessing task.state: {type(state_exc).__name__}: {str(state_exc)}")
|
||||
return Response({
|
||||
'state': 'UNKNOWN',
|
||||
'meta': {
|
||||
'error': f'Error accessing task: {str(state_exc)}',
|
||||
'percentage': 0,
|
||||
'message': f'Error: {str(state_exc)}',
|
||||
}
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'UNKNOWN',
|
||||
'meta': {
|
||||
'error': f'Error accessing task: {str(state_exc)}',
|
||||
'percentage': 0,
|
||||
'message': f'Error: {str(state_exc)}',
|
||||
}
|
||||
},
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Check if task exists and is accessible
|
||||
if task_state is None:
|
||||
# Task doesn't exist or hasn't been registered yet
|
||||
return Response({
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task not found or not yet registered',
|
||||
'phase': 'initializing',
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task not found or not yet registered',
|
||||
'phase': 'initializing',
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
# Safely get task info/result
|
||||
# Try to get error from multiple sources
|
||||
@@ -1114,10 +1163,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
# Include image_queue if available (for image generation)
|
||||
if 'image_queue' in meta:
|
||||
response_meta['image_queue'] = meta['image_queue']
|
||||
return Response({
|
||||
'state': task_state,
|
||||
'meta': response_meta
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': task_state,
|
||||
'meta': response_meta
|
||||
},
|
||||
request=request
|
||||
)
|
||||
elif task_state == 'SUCCESS':
|
||||
result = task_result or {}
|
||||
meta = result if isinstance(result, dict) else {}
|
||||
@@ -1133,10 +1185,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
response_meta['request_steps'] = meta['request_steps']
|
||||
if 'response_steps' in meta:
|
||||
response_meta['response_steps'] = meta['response_steps']
|
||||
return Response({
|
||||
'state': task_state,
|
||||
'meta': response_meta
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': task_state,
|
||||
'meta': response_meta
|
||||
},
|
||||
request=request
|
||||
)
|
||||
elif task_state == 'FAILURE':
|
||||
# Try to get error from task.info meta first (this is where run_ai_task sets it)
|
||||
if not error_message and isinstance(task_info, dict):
|
||||
@@ -1211,42 +1266,55 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
error_type = meta['error_type']
|
||||
response_meta['error_type'] = error_type
|
||||
|
||||
return Response({
|
||||
'state': task_state,
|
||||
'meta': response_meta
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': task_state,
|
||||
'meta': response_meta
|
||||
},
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
# PENDING, STARTED, or other states
|
||||
return Response({
|
||||
'state': task_state,
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is starting...',
|
||||
'phase': 'initializing',
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': task_state,
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is starting...',
|
||||
'phase': 'initializing',
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except (KombuOperationalError, RedisConnectionError, ConnectionError) as conn_error:
|
||||
# Backend connection error - task might not be registered yet or backend is down
|
||||
logger.warning(f"Backend connection error for task {task_id}: {type(conn_error).__name__}: {str(conn_error)}")
|
||||
return Response({
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is being queued...',
|
||||
'phase': 'initializing',
|
||||
'error': None # Don't show as error, just pending
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is being queued...',
|
||||
'phase': 'initializing',
|
||||
'error': None # Don't show as error, just pending
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except Exception as task_error:
|
||||
logger.error(f"Error accessing Celery task {task_id}: {type(task_error).__name__}: {str(task_error)}", exc_info=True)
|
||||
return Response({
|
||||
'state': 'UNKNOWN',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': f'Error accessing task: {str(task_error)}',
|
||||
'error': str(task_error)
|
||||
}
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'UNKNOWN',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': f'Error accessing task: {str(task_error)}',
|
||||
'error': str(task_error)
|
||||
}
|
||||
},
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Check if it's a connection-related error - treat as PENDING instead of error
|
||||
@@ -1263,19 +1331,22 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
if is_connection_error:
|
||||
logger.warning(f"Connection error getting task progress for {task_id}: {error_type}: {str(e)}")
|
||||
return Response({
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is being queued...',
|
||||
'phase': 'initializing',
|
||||
'error': None
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'PENDING',
|
||||
'meta': {
|
||||
'percentage': 0,
|
||||
'message': 'Task is being queued...',
|
||||
'phase': 'initializing',
|
||||
'error': None
|
||||
}
|
||||
},
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
logger.error(f"Error getting task progress for {task_id}: {error_type}: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{
|
||||
return success_response(
|
||||
data={
|
||||
'state': 'ERROR',
|
||||
'meta': {
|
||||
'error': f'Error getting task status: {str(e)}',
|
||||
@@ -1283,6 +1354,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
'message': f'Error: {str(e)}'
|
||||
}
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ URL patterns for system module.
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, system_status, get_request_metrics, gitea_webhook
|
||||
from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, system_status, get_request_metrics, gitea_webhook, ping
|
||||
from .integration_views import IntegrationSettingsViewSet
|
||||
from .settings_views import (
|
||||
SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet,
|
||||
@@ -51,6 +51,8 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
# Public health check endpoint (API Standard v1.0 requirement)
|
||||
path('ping/', ping, name='system-ping'),
|
||||
# System status endpoint
|
||||
path('status/', system_status, name='system-status'),
|
||||
# Request metrics endpoint
|
||||
|
||||
@@ -269,6 +269,24 @@ class StrategyViewSet(AccountModelViewSet):
|
||||
filterset_fields = ['is_active', 'sector']
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny]) # Public endpoint
|
||||
@extend_schema(
|
||||
tags=['System'],
|
||||
summary='Health Check',
|
||||
description='Simple health check endpoint to verify API is responding'
|
||||
)
|
||||
def ping(request):
|
||||
"""
|
||||
Simple health check endpoint
|
||||
Returns unified format: {success: true, data: {status: 'ok'}}
|
||||
"""
|
||||
return success_response(
|
||||
data={'status': 'ok'},
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny]) # Public endpoint for monitoring
|
||||
def system_status(request):
|
||||
|
||||
Reference in New Issue
Block a user