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:
IGNY8 VPS (Salman)
2025-11-16 07:01:19 +00:00
parent 201bc339a8
commit 7cd0e1a807
9 changed files with 376 additions and 308 deletions

View File

@@ -29,7 +29,9 @@ def postprocess_schema_filter_tags(result, generator, request, public):
# If no explicit tags found, infer from path # If no explicit tags found, infer from path
if not filtered_tags: if not filtered_tags:
if '/auth/' in path or '/api/v1/auth/' in path: if '/ping' in path or '/system/ping/' in path:
filtered_tags = ['System'] # Health check endpoint
elif '/auth/' in path or '/api/v1/auth/' in path:
filtered_tags = ['Authentication'] filtered_tags = ['Authentication']
elif '/planner/' in path or '/api/v1/planner/' in path: elif '/planner/' in path or '/api/v1/planner/' in path:
filtered_tags = ['Planner'] filtered_tags = ['Planner']

View File

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

View File

@@ -3,7 +3,7 @@ URL patterns for system module.
""" """
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter 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 .integration_views import IntegrationSettingsViewSet
from .settings_views import ( from .settings_views import (
SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet, SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet,
@@ -51,6 +51,8 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
# Public health check endpoint (API Standard v1.0 requirement)
path('ping/', ping, name='system-ping'),
# System status endpoint # System status endpoint
path('status/', system_status, name='system-status'), path('status/', system_status, name='system-status'),
# Request metrics endpoint # Request metrics endpoint

View File

@@ -269,6 +269,24 @@ class StrategyViewSet(AccountModelViewSet):
filterset_fields = ['is_active', 'sector'] 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']) @api_view(['GET'])
@permission_classes([AllowAny]) # Public endpoint for monitoring @permission_classes([AllowAny]) # Public endpoint for monitoring
def system_status(request): def system_status(request):

View File

@@ -268,8 +268,12 @@ SPECTACULAR_SETTINGS = {
## Authentication ## Authentication
All endpoints require JWT Bearer token authentication except: All endpoints require JWT Bearer token authentication except:
- `GET /api/v1/system/ping/` - Health check endpoint
- `POST /api/v1/auth/login/` - User login - `POST /api/v1/auth/login/` - User login
- `POST /api/v1/auth/register/` - User registration - `POST /api/v1/auth/register/` - User registration
- `GET /api/v1/auth/plans/` - List subscription plans
- `GET /api/v1/auth/industries/` - List industries
- `GET /api/v1/system/status/` - System status
Include token in Authorization header: Include token in Authorization header:
``` ```

View File

@@ -138,6 +138,8 @@ export default function ImageGenerationCard({
console.log('[ImageGenerationCard] Making request to image generation endpoint'); console.log('[ImageGenerationCard] Making request to image generation endpoint');
console.log('[ImageGenerationCard] Request body:', requestBody); console.log('[ImageGenerationCard] Request body:', requestBody);
// fetchAPI extracts data from unified format {success: true, data: {...}}
// So data is the extracted response payload
const data = await fetchAPI('/v1/system/settings/integrations/image_generation/generate/', { const data = await fetchAPI('/v1/system/settings/integrations/image_generation/generate/', {
method: 'POST', method: 'POST',
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
@@ -145,8 +147,10 @@ export default function ImageGenerationCard({
console.log('[ImageGenerationCard] Response data:', data); console.log('[ImageGenerationCard] Response data:', data);
if (!data.success) { // fetchAPI extracts data from unified format, so data is the response payload
throw new Error(data.error || 'Failed to generate image'); // If fetchAPI didn't throw, the request was successful
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format');
} }
const imageData = { const imageData = {

View File

@@ -81,38 +81,30 @@ export default function ValidationCard({
}; };
} }
// Test endpoint returns Response({success: True, ...}) directly (not unified format) // Test endpoint now returns unified format {success: true, data: {...}}
// So fetchAPI may or may not extract it - handle both cases // fetchAPI extracts the data field, so data is the inner object
const data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, { const data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, {
method: 'POST', method: 'POST',
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
}); });
// Check if data has success field (direct Response format) or is extracted data // fetchAPI extracts data from unified format, so data is the response payload
if (data && typeof data === 'object' && ('success' in data ? data.success : true)) { if (data && typeof data === 'object') {
// If data has success field, use it; otherwise assume success (extracted data) // Success response - data contains message, model_used, response, etc.
const isSuccess = data.success !== false; setTestResult({
if (isSuccess) { success: true,
setTestResult({ message: data.message || 'API connection successful!',
success: true, model_used: data.model_used || data.model,
message: data.message || 'API connection successful!', response: data.response,
model_used: data.model_used || data.model, tokens_used: data.tokens_used,
response: data.response, total_tokens: data.total_tokens,
tokens_used: data.tokens_used, cost: data.cost,
total_tokens: data.total_tokens, full_response: data.full_response || {
cost: data.cost, image_url: data.image_url,
full_response: data.full_response || { provider: data.provider,
image_url: data.image_url, size: data.size,
provider: data.provider, },
size: data.size, });
},
});
} else {
setTestResult({
success: false,
message: data.error || data.message || 'API connection failed',
});
}
} else { } else {
setTestResult({ setTestResult({
success: false, success: false,

View File

@@ -334,8 +334,7 @@ export default function Integration() {
try { try {
// fetchAPI extracts data from unified format {success: true, data: {...}} // fetchAPI extracts data from unified format {success: true, data: {...}}
// But test endpoint may return {success: true, ...} directly (not wrapped) // So data is the extracted response payload
// So data could be either the extracted data object or the full response
const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, { const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@@ -344,48 +343,23 @@ export default function Integration() {
}), }),
}); });
// Handle both unified format (extracted) and direct format // fetchAPI extracts data from unified format, so data is the response payload
// If data has success field, it's the direct response (not extracted)
// If data doesn't have success but has other fields, it's extracted data (successful)
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
if (data.success === true || data.success === false) { // Success response - data contains message, response, tokens_used, etc.
// Direct response format (not extracted by fetchAPI) toast.success(data.message || 'API connection test successful!');
if (data.success) { if (data.response) {
toast.success(data.message || 'API connection test successful!'); toast.info(`Response: ${data.response}`);
if (data.response) { }
toast.info(`Response: ${data.response}`); if (data.tokens_used) {
} toast.info(`Tokens used: ${data.tokens_used}`);
if (data.tokens_used) { }
toast.info(`Tokens used: ${data.tokens_used}`);
}
// Update validation status to success // Update validation status to success
if (selectedIntegration) { if (selectedIntegration) {
setValidationStatuses(prev => ({ setValidationStatuses(prev => ({
...prev, ...prev,
[selectedIntegration]: 'success', [selectedIntegration]: 'success',
})); }));
}
} else {
throw new Error(data.error || data.message || 'Connection test failed');
}
} else {
// Extracted data format (successful response)
toast.success('API connection test successful!');
if (data.response) {
toast.info(`Response: ${data.response}`);
}
if (data.tokens_used) {
toast.info(`Tokens used: ${data.tokens_used}`);
}
// Update validation status to success
if (selectedIntegration) {
setValidationStatuses(prev => ({
...prev,
[selectedIntegration]: 'success',
}));
}
} }
} else { } else {
throw new Error('Invalid response format'); throw new Error('Invalid response format');

View File

@@ -144,7 +144,7 @@ Authorization: Bearer <access_token>
- `GET /api/v1/auth/plans/` - `GET /api/v1/auth/plans/`
- `GET /api/v1/auth/industries/` - `GET /api/v1/auth/industries/`
- `GET /api/v1/system/status/` - `GET /api/v1/system/status/`
- `GET /api/ping/` (health check) - `GET /api/v1/system/ping/` (health check)
**All other endpoints require JWT authentication.** **All other endpoints require JWT authentication.**