diff --git a/CLAUDE.md b/CLAUDE.md index 8922a0c..3964600 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -681,4 +681,4 @@ curl http://localhost:8000/health/ Always run `python manage.py check_db` before making database-related changes to ensure proper configuration. --- -Last updated: Last updated: Last updated: Last updated: Last updated: 2025-07-28 22:35:24 +Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: 2025-07-29 19:33:01 diff --git a/agent_base/__init__.py b/agent_base/__init__.py deleted file mode 100644 index c94a4a0..0000000 --- a/agent_base/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Agent Base Framework \ No newline at end of file diff --git a/agent_base/admin.py b/agent_base/admin.py deleted file mode 100644 index 594d8fe..0000000 --- a/agent_base/admin.py +++ /dev/null @@ -1,74 +0,0 @@ -from django.contrib import admin -from .models import BaseAgent - - -@admin.register(BaseAgent) -class BaseAgentAdmin(admin.ModelAdmin): - list_display = ['name', 'slug', 'is_active', 'price', 'agent_type', 'created_at'] - list_display_links = ['name', 'slug'] # Make these clickable for editing - list_filter = ['is_active', 'agent_type', 'category', 'created_at'] - search_fields = ['name', 'slug', 'description'] - readonly_fields = ['slug', 'created_at', 'updated_at'] - ordering = ['name'] - list_editable = ['is_active', 'price'] # Allow quick editing in list view - list_per_page = 25 - - fieldsets = ( - ('Basic Information', { - 'fields': ('name', 'slug', 'description', 'category', 'agent_type'), - 'description': 'Core agent information and classification' - }), - ('Pricing & Display', { - 'fields': ('price', 'icon', 'is_active'), - 'description': 'Pricing and visual configuration' - }), - ('Statistics', { - 'fields': ('rating', 'review_count'), - 'classes': ('collapse',), - 'description': 'Agent performance metrics' - }), - ('Timestamps', { - 'fields': ('created_at', 'updated_at'), - 'classes': ('collapse',), - 'description': 'Creation and modification dates' - }), - ) - - actions = ['activate_agents', 'deactivate_agents', 'reset_ratings'] - - def get_readonly_fields(self, request, obj=None): - if obj: # editing an existing object - return self.readonly_fields + ('agent_type',) - return self.readonly_fields - - def price_display(self, obj): - return f"{obj.price} AED" - price_display.short_description = 'Price' - price_display.admin_order_field = 'price' - - def activate_agents(self, request, queryset): - updated = queryset.update(is_active=True) - self.message_user(request, f'{updated} agents were successfully activated.') - activate_agents.short_description = "Activate selected agents" - - def deactivate_agents(self, request, queryset): - updated = queryset.update(is_active=False) - self.message_user(request, f'{updated} agents were successfully deactivated.') - deactivate_agents.short_description = "Deactivate selected agents" - - def reset_ratings(self, request, queryset): - updated = queryset.update(rating=4.5, review_count=0) - self.message_user(request, f'{updated} agents had their ratings reset.') - reset_ratings.short_description = "Reset ratings to default" - - def has_add_permission(self, request): - return True - - def has_change_permission(self, request, obj=None): - return True - - def has_delete_permission(self, request, obj=None): - return True - - def has_view_permission(self, request, obj=None): - return True \ No newline at end of file diff --git a/agent_base/apps.py b/agent_base/apps.py deleted file mode 100644 index 224f3e9..0000000 --- a/agent_base/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class AgentBaseConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'agent_base' - verbose_name = 'Agent Base Framework' \ No newline at end of file diff --git a/agent_base/generators/__init__.py b/agent_base/generators/__init__.py deleted file mode 100644 index 2f53723..0000000 --- a/agent_base/generators/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# {{ agent_name }} Agent App \ No newline at end of file diff --git a/agent_base/generators/admin.py b/agent_base/generators/admin.py deleted file mode 100644 index f77738b..0000000 --- a/agent_base/generators/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin -from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response - - -@admin.register({{ agent_name_camel }}Request) -class {{ agent_name_camel }}RequestAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'status', 'created_at', 'cost'] - list_filter = ['status', 'created_at'] - search_fields = ['user__email', 'user__username'] - readonly_fields = ['id', 'created_at', 'processed_at'] - ordering = ['-created_at'] - - -@admin.register({{ agent_name_camel }}Response) -class {{ agent_name_camel }}ResponseAdmin(admin.ModelAdmin): - list_display = ['id', 'request', 'success', 'created_at'] - list_filter = ['success', 'created_at'] - readonly_fields = ['id', 'created_at'] - ordering = ['-created_at'] \ No newline at end of file diff --git a/agent_base/generators/api_models.py b/agent_base/generators/api_models.py deleted file mode 100644 index 13cf168..0000000 --- a/agent_base/generators/api_models.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import models -from decimal import Decimal -from agent_base.models import BaseAgentRequest, BaseAgentResponse - - -class {{ agent_name_camel }}Request(BaseAgentRequest): - """{{ agent_name }} request tracking""" - - # Agent-specific request fields - {% for field in request_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) - {% endfor %} - - class Meta: - db_table = '{{ agent_slug_underscore }}_requests' - verbose_name = '{{ agent_name }} Request' - verbose_name_plural = '{{ agent_name }} Requests' - - -class {{ agent_name_camel }}Response(BaseAgentResponse): - """{{ agent_name }} response storage""" - - request = models.OneToOneField( - {{ agent_name_camel }}Request, - on_delete=models.CASCADE, - related_name='response' - ) - - # Agent-specific response fields - {% for field in response_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) - {% endfor %} - - class Meta: - db_table = '{{ agent_slug_underscore }}_responses' - verbose_name = '{{ agent_name }} Response' - verbose_name_plural = '{{ agent_name }} Responses' \ No newline at end of file diff --git a/agent_base/generators/api_processor.py b/agent_base/generators/api_processor.py deleted file mode 100644 index adf3dda..0000000 --- a/agent_base/generators/api_processor.py +++ /dev/null @@ -1,103 +0,0 @@ -from agent_base.processors import StandardAPIProcessor -from django.utils import timezone -from django.conf import settings -from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response -import json - - -class {{ agent_name_camel }}Processor(StandardAPIProcessor): - """API processor for {{ agent_name }} agent""" - - agent_slug = '{{ agent_slug }}' - api_base_url = '{{ api_base_url }}' - api_key_env = '{{ api_key_env }}' - auth_method = '{{ auth_method }}' - - def prepare_request_data(self, **kwargs): - """Prepare API request data""" - {% if api_params %}data = {} - {% for param in api_params %}data['{{ param.name }}'] = kwargs.get('{{ param.value }}', '') - {% endfor %}return data{% else %}return { - 'query': kwargs.get('query', ''), - }{% endif %} - - def should_use_get(self, **kwargs): - """Use GET method for API calls""" - return {{ use_get_method }} - - def build_url(self, **kwargs): - """Build the complete API URL""" - {% if endpoint_params %}url = self.api_base_url - {% for param in endpoint_params %}url = url.replace('{{'{{ param.name }}}}', str(kwargs.get('{{ param.name }}', ''))) - {% endfor %}return url{% else %}return self.api_base_url{% endif %} - - def process_response(self, response_data, request_obj): - """Process the API response""" - try: - request_obj.status = 'processing' - request_obj.save() - - # Extract response data - {% for field in response_processing %}{% if field.source %}{{ field.name }} = self.get_nested_value(response_data, '{{ field.source }}') or {{ field.default }}{% else %}{{ field.name }} = response_data if response_data else {{ field.default }}{% endif %} - {% endfor %} - - # Determine success based on response (check for valid data) - success = (response_data.get('success', True) if isinstance(response_data, dict) else True) and bool(response_data) - - # Create response object - response_obj = {{ agent_name_camel }}Response.objects.create( - request=request_obj, - success=success, - processing_time=response_data.get('processing_time', 0) if isinstance(response_data, dict) else 0, - {% for field in response_processing %}{{ field.name }}={{ field.name }}, - {% endfor %} - ) - - # Only deduct wallet balance after successful processing - if success: - request_obj.user.deduct_balance( - request_obj.cost, - f"{{ agent_name }} - API Request", - '{{ agent_slug }}' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") - - # Update request as completed - request_obj.status = 'completed' if success else 'failed' - request_obj.processed_at = timezone.now() - request_obj.save() - - return response_obj - - except Exception as e: - # Handle error - request_obj.status = 'failed' - request_obj.save() - - # Create error response - error_response = {{ agent_name_camel }}Response.objects.create( - request=request_obj, - success=False, - error_message=str(e), - processing_time=0 - ) - - raise Exception(f"Failed to process {{ agent_name }} response: {e}") - - def get_nested_value(self, data, path): - """Get nested value from dictionary using dot notation""" - if not path or not isinstance(data, dict): - return None - - keys = path.split('.') - value = data - - for key in keys: - if isinstance(value, dict) and key in value: - value = value[key] - elif isinstance(value, list) and key.isdigit() and int(key) < len(value): - value = value[int(key)] - else: - return None - - return value \ No newline at end of file diff --git a/agent_base/generators/apps.py b/agent_base/generators/apps.py deleted file mode 100644 index cd2dbe2..0000000 --- a/agent_base/generators/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class {{ agent_name_camel }}Config(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = '{{ agent_slug_underscore }}' \ No newline at end of file diff --git a/agent_base/generators/urls.py b/agent_base/generators/urls.py deleted file mode 100644 index 29d437b..0000000 --- a/agent_base/generators/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from . import views - -app_name = '{{ agent_slug_underscore }}' - -urlpatterns = [ - path('', views.{{ agent_slug_underscore }}_detail, name='detail'), - path('process/', views.{{ agent_name_camel }}ProcessView.as_view(), name='process'), - path('result//', views.{{ agent_slug_underscore }}_result, name='result'), -] \ No newline at end of file diff --git a/agent_base/generators/views.py b/agent_base/generators/views.py deleted file mode 100644 index f6746dc..0000000 --- a/agent_base/generators/views.py +++ /dev/null @@ -1,123 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator -from django.views import View -from agent_base.models import BaseAgent -from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response -from .processor import {{ agent_name_camel }}Processor -import json - - -@login_required -def {{ agent_slug_underscore }}_detail(request): - """Detail page for {{ agent_name }} agent""" - try: - agent = BaseAgent.objects.get(slug='{{ agent_slug }}') - except BaseAgent.DoesNotExist: - messages.error(request, '{{ agent_name }} agent not found.') - return redirect('core:homepage') - - # Get user's recent requests - user_requests = {{ agent_name_camel }}Request.objects.filter( - user=request.user - ).order_by('-created_at')[:10] - - context = { - 'agent': agent, - 'user_requests': user_requests - } - return render(request, '{{ agent_slug_underscore }}/detail.html', context) - - -@method_decorator(csrf_exempt, name='dispatch') -class {{ agent_name_camel }}ProcessView(View): - """Process {{ agent_name }} requests""" - - def post(self, request): - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - # Parse request data - {% if agent_type == 'api' and 'pdf' in agent_slug %}# Handle multipart form data for file uploads - data = request.POST.dict() - files = request.FILES - {% else %}data = json.loads(request.body){% endif %} - - # Get agent - agent = BaseAgent.objects.get(slug='{{ agent_slug }}') - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - # Create request object (no wallet deduction yet - only after successful processing) - agent_request = {{ agent_name_camel }}Request.objects.create( - user=request.user, - agent=agent, - cost=agent.price, - {% for field in request_creation %}{{ field.name }}=data.get('{{ field.source }}', '{{ field.default }}'), - {% endfor %} - ) - - # Process request - processor = {{ agent_name_camel }}Processor() - result = processor.process_request( - request_obj=agent_request, - user_id=request.user.id, - {% for param in processor_params %}{{ param.name }}=data.get('{{ param.source }}'), - {% endfor %} - ) - - # Refresh user from database to get updated wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': True, - 'request_id': str(agent_request.id), - 'message': '{{ agent_name }} request processed successfully', - 'wallet_balance': float(request.user.wallet_balance) - }) - - except BaseAgent.DoesNotExist: - return JsonResponse({'error': '{{ agent_name }} agent not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -@login_required -def {{ agent_slug_underscore }}_result(request, request_id): - """Get result for a specific request""" - try: - agent_request = {{ agent_name_camel }}Request.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': response.success, - 'status': agent_request.status, - {% for field in result_fields %}'{{ field.name }}': getattr(response, '{{ field.name }}', None), - {% endfor %}'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except {{ agent_name_camel }}Request.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/agent_base/generators/weather_api_processor.py b/agent_base/generators/weather_api_processor.py deleted file mode 100644 index 74beb03..0000000 --- a/agent_base/generators/weather_api_processor.py +++ /dev/null @@ -1,140 +0,0 @@ -from agent_base.processors import StandardAPIProcessor -from django.utils import timezone -from django.conf import settings -from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response -import json - - -class {{ agent_name_camel }}Processor(StandardAPIProcessor): - """Weather API processor for {{ agent_name }} agent""" - - agent_slug = '{{ agent_slug }}' - api_base_url = '{{ api_base_url }}' - api_key_env = '{{ api_key_env }}' - auth_method = '{{ auth_method }}' - - def prepare_request_data(self, **kwargs): - """Prepare weather API request data""" - return { - 'q': kwargs.get('location', ''), - 'units': 'metric', - 'appid': self.get_api_key() - } - - def should_use_get(self, **kwargs): - """Use GET method for weather API calls""" - return True - - def build_url(self, **kwargs): - """Build the complete weather API URL""" - location = kwargs.get('location', '') - base_url = self.api_base_url - if '?' not in base_url: - base_url += '?' - return base_url - - def process_response(self, response_data, request_obj): - """Process the weather API response""" - try: - request_obj.status = 'processing' - request_obj.save() - - # Extract weather data - weather_data = response_data if response_data else {} - temperature = self.get_nested_value(response_data, 'main.temp') - description = self.get_nested_value(response_data, 'weather.0.description') or '' - humidity = self.get_nested_value(response_data, 'main.humidity') - wind_speed = self.get_nested_value(response_data, 'wind.speed') - - # Generate formatted report - formatted_report = self.generate_weather_report( - weather_data, - request_obj.location, - request_obj.report_type - ) - - # Determine success based on weather data availability - success = bool(weather_data.get('main')) and temperature is not None - - # Create response object - response_obj = {{ agent_name_camel }}Response.objects.create( - request=request_obj, - success=success, - processing_time=0, - weather_data=weather_data, - temperature=temperature, - description=description.title() if description else '', - humidity=humidity, - wind_speed=wind_speed, - formatted_report=formatted_report, - ) - - # Only deduct wallet balance after successful processing - if success: - request_obj.user.deduct_balance( - request_obj.cost, - f"{{ agent_name }} - Weather for {request_obj.location}", - '{{ agent_slug }}' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") - - # Update request as completed - request_obj.status = 'completed' if success else 'failed' - request_obj.processed_at = timezone.now() - request_obj.save() - - return response_obj - - except Exception as e: - # Handle error - request_obj.status = 'failed' - request_obj.save() - - # Create error response - error_response = {{ agent_name_camel }}Response.objects.create( - request=request_obj, - success=False, - error_message=str(e), - processing_time=0 - ) - - raise Exception(f"Failed to process weather response: {e}") - - def generate_weather_report(self, weather_data, location, report_type): - """Generate formatted weather report""" - if not weather_data or 'main' not in weather_data: - return f"Unable to get weather data for {location}" - - temp = weather_data.get('main', {}).get('temp', 'N/A') - description = weather_data.get('weather', [{}])[0].get('description', 'N/A') - humidity = weather_data.get('main', {}).get('humidity', 'N/A') - wind_speed = weather_data.get('wind', {}).get('speed', 'N/A') - feels_like = weather_data.get('main', {}).get('feels_like', 'N/A') - - if report_type == 'current': - return f"Current weather in {location}: {description.title()}, {temp}°C" - else: - return f"""Weather Report for {location}: - -šŸŒ”ļø Temperature: {temp}°C (feels like {feels_like}°C) -šŸŒ¤ļø Conditions: {description.title()} -šŸ’§ Humidity: {humidity}% -šŸ’Ø Wind Speed: {wind_speed} m/s""" - - def get_nested_value(self, data, path): - """Get nested value from dictionary using dot notation""" - if not path or not isinstance(data, dict): - return None - - keys = path.split('.') - value = data - - for key in keys: - if isinstance(value, dict) and key in value: - value = value[key] - elif isinstance(value, list) and key.isdigit() and int(key) < len(value): - value = value[int(key)] - else: - return None - - return value \ No newline at end of file diff --git a/agent_base/generators/webhook_models.py b/agent_base/generators/webhook_models.py deleted file mode 100644 index 13cf168..0000000 --- a/agent_base/generators/webhook_models.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import models -from decimal import Decimal -from agent_base.models import BaseAgentRequest, BaseAgentResponse - - -class {{ agent_name_camel }}Request(BaseAgentRequest): - """{{ agent_name }} request tracking""" - - # Agent-specific request fields - {% for field in request_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) - {% endfor %} - - class Meta: - db_table = '{{ agent_slug_underscore }}_requests' - verbose_name = '{{ agent_name }} Request' - verbose_name_plural = '{{ agent_name }} Requests' - - -class {{ agent_name_camel }}Response(BaseAgentResponse): - """{{ agent_name }} response storage""" - - request = models.OneToOneField( - {{ agent_name_camel }}Request, - on_delete=models.CASCADE, - related_name='response' - ) - - # Agent-specific response fields - {% for field in response_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) - {% endfor %} - - class Meta: - db_table = '{{ agent_slug_underscore }}_responses' - verbose_name = '{{ agent_name }} Response' - verbose_name_plural = '{{ agent_name }} Responses' \ No newline at end of file diff --git a/agent_base/generators/webhook_processor.py b/agent_base/generators/webhook_processor.py deleted file mode 100644 index 4fda657..0000000 --- a/agent_base/generators/webhook_processor.py +++ /dev/null @@ -1,70 +0,0 @@ -from agent_base.processors import StandardWebhookProcessor -from django.utils import timezone -from django.conf import settings -from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response -import json - - -class {{ agent_name_camel }}Processor(StandardWebhookProcessor): - """Webhook processor for {{ agent_name }} agent""" - - agent_slug = '{{ agent_slug }}' - webhook_url = settings.N8N_WEBHOOK_{{ agent_slug_underscore|upper }} - agent_id = '{{ agent_id }}' - - def prepare_message_text(self, **kwargs): - """Prepare message for N8N webhook""" - return "{{ message_format }}".format(**kwargs) - - def process_response(self, response_data, request_obj): - """Process webhook response""" - try: - request_obj.status = 'processing' - request_obj.save() - - # Extract response data - {% for field in response_processing %}{% if field.source %}{{ field.name }} = response_data.get('{{ field.source }}', {{ field.default }}){% else %}{{ field.name }} = response_data if response_data else {{ field.default }}{% endif %} - {% endfor %} - - # Determine success based on response - success = response_data.get('success', True) and response_data.get('status') == 'success' - - # Create response object - response_obj = {{ agent_name_camel }}Response.objects.create( - request=request_obj, - success=success, - processing_time=response_data.get('processing_time', 0), - {% for field in response_processing %}{{ field.name }}={{ field.name }}, - {% endfor %} - ) - - # Only deduct wallet balance after successful processing - if success: - request_obj.user.deduct_balance( - request_obj.cost, - f"{{ agent_name }} - Processing", - '{{ agent_slug }}' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") - - # Update request as completed - request_obj.status = 'completed' if success else 'failed' - request_obj.processed_at = timezone.now() - request_obj.save() - - return response_obj - - except Exception as e: - # Handle error - request_obj.status = 'failed' - request_obj.save() - - # Create error response - error_response = {{ agent_name_camel }}Response.objects.create( - request=request_obj, - success=False, - error_message=str(e), - processing_time=0 - ) - - raise Exception(f"Failed to process {{ agent_name }} response: {e}") \ No newline at end of file diff --git a/agent_base/management/__init__.py b/agent_base/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agent_base/management/commands/__init__.py b/agent_base/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agent_base/management/commands/backup_users.py b/agent_base/management/commands/backup_users.py deleted file mode 100644 index 336e725..0000000 --- a/agent_base/management/commands/backup_users.py +++ /dev/null @@ -1,211 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from wallet.models import WalletTransaction -import json -from decimal import Decimal - -User = get_user_model() - - -class Command(BaseCommand): - help = 'Backup and restore user data for Railway deployments' - - def add_arguments(self, parser): - parser.add_argument( - '--action', - choices=['backup', 'restore', 'info'], - default='info', - help='Action to perform: backup, restore, or info', - ) - parser.add_argument( - '--file', - default='users_backup.json', - help='Backup file path', - ) - - def handle(self, *args, **options): - action = options['action'] - backup_file = options['file'] - - if action == 'info': - self.show_database_info() - elif action == 'backup': - self.backup_users(backup_file) - elif action == 'restore': - self.restore_users(backup_file) - - def show_database_info(self): - """Show current database state""" - self.stdout.write("=== DATABASE INFO ===") - - # Database backend - from django.conf import settings - from django.db import connection - db_config = settings.DATABASES['default'] - self.stdout.write(f"Database Engine: {db_config['ENGINE']}") - if 'NAME' in db_config: - self.stdout.write(f"Database Name: {db_config['NAME']}") - - # Check if tables exist - try: - with connection.cursor() as cursor: - cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") - tables = [row[0] for row in cursor.fetchall()] - self.stdout.write(f"Database tables: {len(tables)} found") - - if 'authentication_user' not in tables: - self.stdout.write("āš ļø User table not found - database not yet migrated") - return - except Exception as e: - self.stdout.write(f"āš ļø Could not check database tables: {e}") - return - - try: - # User counts - total_users = User.objects.count() - superusers = User.objects.filter(is_superuser=True).count() - regular_users = total_users - superusers - - self.stdout.write(f"Total Users: {total_users}") - self.stdout.write(f"Superusers: {superusers}") - self.stdout.write(f"Regular Users: {regular_users}") - - # List superusers - if superusers > 0: - self.stdout.write("\\nSuperusers:") - for user in User.objects.filter(is_superuser=True): - self.stdout.write(f" - {user.email} (username: {user.username})") - - # Wallet info - total_transactions = WalletTransaction.objects.count() - self.stdout.write(f"\\nWallet Transactions: {total_transactions}") - - # Users with positive balance - users_with_balance = User.objects.filter(wallet_balance__gt=0).count() - self.stdout.write(f"Users with balance: {users_with_balance}") - - except Exception as e: - self.stdout.write(f"āš ļø Could not read user data: {e}") - self.stdout.write("Database may not be fully migrated yet") - - def backup_users(self, backup_file): - """Backup all users and their wallet data""" - self.stdout.write(f"Backing up users to {backup_file}...") - - backup_data = { - 'users': [], - 'transactions': [] - } - - # Backup users - for user in User.objects.all(): - user_data = { - 'username': user.username, - 'email': user.email, - 'first_name': user.first_name, - 'last_name': user.last_name, - 'is_superuser': user.is_superuser, - 'is_staff': user.is_staff, - 'is_active': user.is_active, - 'wallet_balance': str(user.wallet_balance), - 'date_joined': user.date_joined.isoformat(), - } - backup_data['users'].append(user_data) - - # Backup transactions - for transaction in WalletTransaction.objects.all(): - transaction_data = { - 'user_email': transaction.user.email, - 'amount': str(transaction.amount), - 'type': transaction.type, - 'description': transaction.description, - 'agent_slug': transaction.agent_slug, - 'stripe_session_id': transaction.stripe_session_id, - 'created_at': transaction.created_at.isoformat(), - } - backup_data['transactions'].append(transaction_data) - - # Write to file - with open(backup_file, 'w') as f: - json.dump(backup_data, f, indent=2) - - self.stdout.write( - self.style.SUCCESS( - f"Backed up {len(backup_data['users'])} users and " - f"{len(backup_data['transactions'])} transactions to {backup_file}" - ) - ) - - def restore_users(self, backup_file): - """Restore users from backup file""" - try: - with open(backup_file, 'r') as f: - backup_data = json.load(f) - except FileNotFoundError: - self.stdout.write( - self.style.ERROR(f"Backup file {backup_file} not found") - ) - return - - self.stdout.write(f"Restoring users from {backup_file}...") - - users_created = 0 - users_updated = 0 - transactions_created = 0 - - # Restore users - for user_data in backup_data.get('users', []): - user, created = User.objects.get_or_create( - email=user_data['email'], - defaults={ - 'username': user_data['username'], - 'first_name': user_data['first_name'], - 'last_name': user_data['last_name'], - 'is_superuser': user_data['is_superuser'], - 'is_staff': user_data['is_staff'], - 'is_active': user_data['is_active'], - 'wallet_balance': Decimal(user_data['wallet_balance']), - } - ) - - if created: - users_created += 1 - self.stdout.write(f"Created user: {user.email}") - else: - # Update wallet balance for existing users - user.wallet_balance = Decimal(user_data['wallet_balance']) - user.save() - users_updated += 1 - self.stdout.write(f"Updated user: {user.email}") - - # Restore transactions - for transaction_data in backup_data.get('transactions', []): - try: - user = User.objects.get(email=transaction_data['user_email']) - transaction, created = WalletTransaction.objects.get_or_create( - user=user, - amount=Decimal(transaction_data['amount']), - type=transaction_data['type'], - description=transaction_data['description'], - created_at=transaction_data['created_at'], - defaults={ - 'agent_slug': transaction_data.get('agent_slug', ''), - 'stripe_session_id': transaction_data.get('stripe_session_id', ''), - } - ) - - if created: - transactions_created += 1 - except User.DoesNotExist: - self.stdout.write( - self.style.WARNING( - f"User {transaction_data['user_email']} not found for transaction" - ) - ) - - self.stdout.write( - self.style.SUCCESS( - f"Restore complete: {users_created} users created, " - f"{users_updated} users updated, {transactions_created} transactions created" - ) - ) \ No newline at end of file diff --git a/agent_base/management/commands/check_db.py b/agent_base/management/commands/check_db.py deleted file mode 100644 index cbdbcdd..0000000 --- a/agent_base/management/commands/check_db.py +++ /dev/null @@ -1,108 +0,0 @@ -from django.core.management.base import BaseCommand -from django.conf import settings -from django.db import connection -import os - - -class Command(BaseCommand): - help = 'Check current database configuration and connection' - - def handle(self, *args, **options): - self.stdout.write("šŸ” Database Configuration Check") - self.stdout.write("=" * 40) - - # Environment detection - is_railway = bool(os.environ.get('RAILWAY_ENVIRONMENT')) - database_url = os.environ.get('DATABASE_URL', '') - - self.stdout.write(f"Environment: {'Railway' if is_railway else 'Local Development'}") - self.stdout.write(f"DATABASE_URL set: {'Yes' if database_url else 'No'}") - - if database_url: - # Mask password in URL for display - masked_url = database_url - if '@' in masked_url and '://' in masked_url: - parts = masked_url.split('://') - if len(parts) == 2: - scheme = parts[0] - rest = parts[1] - if '@' in rest: - auth_part, host_part = rest.split('@', 1) - if ':' in auth_part: - user, password = auth_part.split(':', 1) - masked_url = f"{scheme}://{user}:***@{host_part}" - self.stdout.write(f"DATABASE_URL: {masked_url}") - - # Current Django database configuration - db_config = settings.DATABASES['default'] - engine = db_config['ENGINE'] - - self.stdout.write(f"\\nCurrent Django Configuration:") - self.stdout.write(f"Engine: {engine}") - - if 'postgresql' in engine: - self.stdout.write(f"Database: {db_config.get('NAME', 'N/A')}") - self.stdout.write(f"Host: {db_config.get('HOST', 'N/A')}") - self.stdout.write(f"Port: {db_config.get('PORT', 'N/A')}") - self.stdout.write(f"User: {db_config.get('USER', 'N/A')}") - elif 'sqlite' in engine: - self.stdout.write(f"Database file: {db_config.get('NAME', 'N/A')}") - - # Test connection - self.stdout.write(f"\\nšŸ”Œ Testing Database Connection...") - try: - with connection.cursor() as cursor: - if 'postgresql' in engine: - cursor.execute("SELECT version();") - version = cursor.fetchone()[0] - self.stdout.write(f"āœ… PostgreSQL Connection: {version}") - elif 'sqlite' in engine: - cursor.execute("SELECT sqlite_version();") - version = cursor.fetchone()[0] - self.stdout.write(f"āœ… SQLite Connection: {version}") - - # Check if tables exist - if 'postgresql' in engine: - cursor.execute(""" - SELECT COUNT(*) FROM information_schema.tables - WHERE table_schema = 'public' - """) - else: - cursor.execute(""" - SELECT COUNT(*) FROM sqlite_master - WHERE type='table' AND name NOT LIKE 'sqlite_%' - """) - - table_count = cursor.fetchone()[0] - self.stdout.write(f"šŸ“Š Database tables: {table_count}") - - if table_count == 0: - self.stdout.write("āš ļø No tables found. Run: python manage.py migrate") - - except Exception as e: - self.stdout.write(f"āŒ Connection failed: {e}") - - if 'postgresql' in engine: - self.stdout.write("\\nšŸ’” PostgreSQL Connection Tips:") - self.stdout.write("1. Install PostgreSQL: brew install postgresql") - self.stdout.write("2. Start PostgreSQL: brew services start postgresql") - self.stdout.write("3. Create database: createdb netcop_hub") - self.stdout.write("4. Create user: createuser netcop_user -P") - self.stdout.write("5. Or use Docker: docker run --name netcop-postgres -e POSTGRES_DB=netcop_hub -e POSTGRES_USER=netcop_user -e POSTGRES_PASSWORD=netcop_pass -p 5432:5432 -d postgres:15") - - # Module availability check - self.stdout.write(f"\\nšŸ“¦ Module Availability:") - try: - import psycopg2 - self.stdout.write("āœ… psycopg2 (PostgreSQL driver) available") - except ImportError: - self.stdout.write("āŒ psycopg2 not available") - - try: - import sqlite3 - self.stdout.write("āœ… sqlite3 available") - except ImportError: - self.stdout.write("āŒ sqlite3 not available") - - self.stdout.write("\\n" + "=" * 40) - self.stdout.write("Database check complete!") \ No newline at end of file diff --git a/agent_base/management/commands/create_agent.py b/agent_base/management/commands/create_agent.py deleted file mode 100644 index 57f482a..0000000 --- a/agent_base/management/commands/create_agent.py +++ /dev/null @@ -1,243 +0,0 @@ -from django.core.management.base import BaseCommand -from django.template import Template, Context -from django.conf import settings -from pathlib import Path -import os -import shutil -from agent_base.models import BaseAgent - - -class Command(BaseCommand): - help = 'Create a new agent with standardized structure' - - def add_arguments(self, parser): - parser.add_argument('agent_name', type=str, help='Name of the agent (e.g., "Weather Reporter")') - parser.add_argument('agent_slug', type=str, help='Slug for the agent (e.g., "weather-reporter")') - parser.add_argument('agent_type', choices=['webhook', 'api'], help='Type of agent: webhook or api') - parser.add_argument('--category', default='utilities', help='Category for the agent') - parser.add_argument('--price', type=float, default=1.0, help='Price for the agent') - parser.add_argument('--description', default='', help='Description for the agent') - parser.add_argument('--icon', default='šŸ¤–', help='Icon for the agent') - - # Webhook specific arguments - parser.add_argument('--webhook-url', help='Webhook URL for webhook agents') - parser.add_argument('--agent-id', help='Agent ID for webhook agents') - - # API specific arguments - parser.add_argument('--api-base-url', help='Base URL for API agents') - parser.add_argument('--api-key-env', help='Environment variable name for API key') - parser.add_argument('--auth-method', default='query', choices=['bearer', 'api-key', 'basic', 'query'], help='Authentication method for API') - - def handle(self, *args, **options): - agent_name = options['agent_name'] - agent_slug = options['agent_slug'] - agent_type = options['agent_type'] - - self.stdout.write(f"Creating {agent_type} agent: {agent_name} ({agent_slug})") - - # Create agent directory - agent_dir = Path(settings.BASE_DIR) / agent_slug.replace('-', '_') - if agent_dir.exists(): - self.stdout.write(self.style.ERROR(f"Agent directory {agent_dir} already exists")) - return - - agent_dir.mkdir() - - # Template directory - template_dir = Path(settings.BASE_DIR) / 'agent_base' / 'templates' / 'agent_generator' - - # Common context for all templates - context = { - 'agent_name': agent_name, - 'agent_slug': agent_slug, - 'agent_slug_underscore': agent_slug.replace('-', '_'), - 'agent_name_camel': self.to_camel_case(agent_name), - 'agent_type': agent_type, - } - - if agent_type == 'webhook': - context.update(self.get_webhook_context(options)) - else: - options['agent_slug'] = agent_slug - context.update(self.get_api_context(options)) - - # Copy and render templates - self.create_file_from_template(template_dir / f'{agent_type}_models.py', agent_dir / 'models.py', context) - - # Use weather-specific processor for weather agents - if agent_type == 'api' and 'weather' in agent_slug.lower(): - self.create_file_from_template(template_dir / 'weather_api_processor.py', agent_dir / 'processor.py', context) - else: - self.create_file_from_template(template_dir / f'{agent_type}_processor.py', agent_dir / 'processor.py', context) - self.create_file_from_template(template_dir / 'views.py', agent_dir / 'views.py', context) - self.create_file_from_template(template_dir / 'urls.py', agent_dir / 'urls.py', context) - self.create_file_from_template(template_dir / 'apps.py', agent_dir / 'apps.py', context) - self.create_file_from_template(template_dir / 'admin.py', agent_dir / 'admin.py', context) - self.create_file_from_template(template_dir / '__init__.py', agent_dir / '__init__.py', context) - - # Create migrations directory - migrations_dir = agent_dir / 'migrations' - migrations_dir.mkdir() - (migrations_dir / '__init__.py').write_text('') - - # Create database entry - BaseAgent.objects.get_or_create( - slug=agent_slug, - defaults={ - 'name': agent_name, - 'description': options.get('description', f'{agent_name} agent'), - 'category': options['category'], - 'price': options['price'], - 'icon': options['icon'], - 'agent_type': agent_type, - 'is_active': True, - } - ) - - self.stdout.write(self.style.SUCCESS(f"Successfully created {agent_name} agent")) - agent_slug_underscore = agent_slug.replace('-', '_') - self.stdout.write(f"Next steps:") - self.stdout.write(f"1. Add '{agent_slug_underscore}' to INSTALLED_APPS in settings.py") - self.stdout.write(f"2. Run: python manage.py makemigrations {agent_slug_underscore}") - self.stdout.write(f"3. Run: python manage.py migrate") - self.stdout.write(f"4. Create agent template in templates/agents/{agent_slug}/detail.html") - self.stdout.write(f"5. Add URL patterns to main urls.py") - - def get_webhook_context(self, options): - """Get context for webhook agents""" - webhook_url = options.get('webhook_url', '') - agent_id = options.get('agent_id', '1') - - return { - 'webhook_url': webhook_url, - 'agent_id': agent_id, - 'request_fields': [ - {'name': 'input_text', 'type': 'TextField', 'args': "blank=True"}, - ], - 'response_fields': [ - {'name': 'output_text', 'type': 'TextField', 'args': "blank=True"}, - {'name': 'raw_response', 'type': 'JSONField', 'args': "default=dict, blank=True"}, - ], - 'message_template': [ - {'name': 'input_text', 'required': True}, - ], - 'message_format': 'Process: {input_text}', - 'additional_fields': [], - 'response_processing': [ - {'name': 'output_text', 'source': 'output', 'default': ''}, - {'name': 'raw_response', 'source': '', 'default': 'dict()'}, - ], - 'request_creation': [ - {'name': 'input_text', 'source': 'input_text', 'default': ''}, - ], - 'processor_params': [ - {'name': 'input_text', 'source': 'input_text'}, - ], - 'result_fields': [ - {'name': 'output_text'}, - {'name': 'raw_response'}, - ], - } - - def get_api_context(self, options): - """Get context for API agents""" - api_base_url = options.get('api_base_url', '') - api_key_env = options.get('api_key_env', '') - auth_method = options.get('auth_method', 'query') - agent_slug = options.get('agent_slug', '') - - # Weather-specific context - if 'weather' in agent_slug.lower(): - return { - 'api_base_url': api_base_url, - 'api_key_env': api_key_env, - 'auth_method': auth_method, - 'endpoint_template': api_base_url + '?q={location}&units=metric', - 'endpoint_params': [ - {'name': 'location'}, - ], - 'api_params': [ - {'name': 'q', 'value': 'location'}, - {'name': 'units', 'value': 'metric'}, - ], - 'use_get_method': 'True', - 'request_fields': [ - {'name': 'location', 'type': 'CharField', 'args': "max_length=200"}, - {'name': 'report_type', 'type': 'CharField', 'args': "max_length=50, choices=[('current', 'Current Weather'), ('detailed', 'Detailed Report')], default='current'"}, - ], - 'response_fields': [ - {'name': 'weather_data', 'type': 'JSONField', 'args': "default=dict, blank=True"}, - {'name': 'temperature', 'type': 'DecimalField', 'args': "max_digits=5, decimal_places=2, null=True, blank=True"}, - {'name': 'description', 'type': 'CharField', 'args': "max_length=200, blank=True"}, - {'name': 'humidity', 'type': 'IntegerField', 'args': "null=True, blank=True"}, - {'name': 'wind_speed', 'type': 'DecimalField', 'args': "max_digits=5, decimal_places=2, null=True, blank=True"}, - {'name': 'formatted_report', 'type': 'TextField', 'args': "blank=True"}, - ], - 'response_processing': [ - {'name': 'weather_data', 'source': '', 'default': 'dict()'}, - {'name': 'temperature', 'source': 'main.temp', 'default': 'None'}, - {'name': 'description', 'source': 'weather.0.description', 'default': ''}, - {'name': 'humidity', 'source': 'main.humidity', 'default': 'None'}, - {'name': 'wind_speed', 'source': 'wind.speed', 'default': 'None'}, - {'name': 'formatted_report', 'source': 'formatted_report', 'default': ''}, - ], - 'request_creation': [ - {'name': 'location', 'source': 'location', 'default': ''}, - {'name': 'report_type', 'source': 'report_type', 'default': 'current'}, - ], - 'processor_params': [ - {'name': 'location', 'source': 'location'}, - {'name': 'report_type', 'source': 'report_type'}, - ], - 'result_fields': [ - {'name': 'weather_data'}, - {'name': 'temperature'}, - {'name': 'description'}, - {'name': 'humidity'}, - {'name': 'wind_speed'}, - {'name': 'formatted_report'}, - ], - } - - # Default API context - return { - 'api_base_url': api_base_url, - 'api_key_env': api_key_env, - 'auth_method': auth_method, - 'endpoint_template': api_base_url, - 'endpoint_params': [], - 'api_params': [], - 'use_get_method': 'True', - 'request_fields': [ - {'name': 'query_param', 'type': 'CharField', 'args': "max_length=200, blank=True"}, - ], - 'response_fields': [ - {'name': 'result_data', 'type': 'JSONField', 'args': "default=dict, blank=True"}, - {'name': 'api_response', 'type': 'TextField', 'args': "blank=True"}, - ], - 'response_processing': [ - {'name': 'result_data', 'source': '', 'default': 'dict()'}, - {'name': 'api_response', 'source': 'result', 'default': ''}, - ], - 'request_creation': [ - {'name': 'query_param', 'source': 'query', 'default': ''}, - ], - 'processor_params': [ - {'name': 'query', 'source': 'query'}, - ], - 'result_fields': [ - {'name': 'result_data'}, - {'name': 'api_response'}, - ], - } - - def to_camel_case(self, text): - """Convert text to CamelCase""" - return ''.join(word.capitalize() for word in text.replace('-', ' ').split()) - - def create_file_from_template(self, template_path, output_path, context): - """Create a file from template""" - template_content = template_path.read_text() - template = Template(template_content) - rendered_content = template.render(Context(context)) - output_path.write_text(rendered_content) \ No newline at end of file diff --git a/agent_base/management/commands/create_user.py b/agent_base/management/commands/create_user.py deleted file mode 100644 index ec90510..0000000 --- a/agent_base/management/commands/create_user.py +++ /dev/null @@ -1,91 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from decimal import Decimal - -User = get_user_model() - - -class Command(BaseCommand): - help = 'Create a user with wallet balance' - - def add_arguments(self, parser): - parser.add_argument('email', help='User email address') - parser.add_argument('password', help='User password') - parser.add_argument( - '--username', - help='Username (defaults to email prefix)', - ) - parser.add_argument( - '--first-name', - default='', - help='First name', - ) - parser.add_argument( - '--last-name', - default='', - help='Last name', - ) - parser.add_argument( - '--balance', - type=float, - default=0.0, - help='Initial wallet balance', - ) - parser.add_argument( - '--superuser', - action='store_true', - help='Create as superuser', - ) - - def handle(self, *args, **options): - email = options['email'] - password = options['password'] - username = options.get('username') or email.split('@')[0] - first_name = options['first_name'] - last_name = options['last_name'] - balance = Decimal(str(options['balance'])) - is_superuser = options['superuser'] - - # Check if user already exists - if User.objects.filter(email=email).exists(): - self.stdout.write( - self.style.ERROR(f"User with email {email} already exists") - ) - return - - # Create user - if is_superuser: - user = User.objects.create_superuser( - username=username, - email=email, - password=password, - first_name=first_name, - last_name=last_name, - ) - user_type = "superuser" - else: - user = User.objects.create_user( - username=username, - email=email, - password=password, - first_name=first_name, - last_name=last_name, - ) - user_type = "user" - - # Set wallet balance if provided - if balance > 0: - user.add_balance(balance, "Initial balance from admin") - - self.stdout.write( - self.style.SUCCESS( - f"Created {user_type}: {email} with balance {balance} AED" - ) - ) - - # Show login instructions - self.stdout.write("\\nLogin credentials:") - self.stdout.write(f"Email: {email}") - self.stdout.write(f"Password: {password}") - if is_superuser: - self.stdout.write("Admin URL: /admin/") \ No newline at end of file diff --git a/agent_base/management/commands/fix_migrations.py b/agent_base/management/commands/fix_migrations.py deleted file mode 100644 index 9619dcc..0000000 --- a/agent_base/management/commands/fix_migrations.py +++ /dev/null @@ -1,121 +0,0 @@ -from django.core.management.base import BaseCommand -from django.core.management import call_command -from django.db import connection -from django.db.migrations.recorder import MigrationRecorder - - -class Command(BaseCommand): - help = 'Fix migration conflicts and sync database state' - - def add_arguments(self, parser): - parser.add_argument( - '--app', - default='data_analyzer', - help='App to fix migrations for (default: data_analyzer)', - ) - parser.add_argument( - '--migration', - default='0002_auto_20250710_0431', - help='Specific migration to mark as fake', - ) - parser.add_argument( - '--check-only', - action='store_true', - help='Only check migration status without fixing', - ) - - def handle(self, *args, **options): - app_label = options['app'] - migration_name = options['migration'] - check_only = options['check_only'] - - self.stdout.write(f"šŸ” Checking migration status for {app_label}...") - - # Check if problematic migration is already applied - recorder = MigrationRecorder(connection) - applied_migrations = recorder.applied_migrations() - - migration_key = (app_label, migration_name) - is_applied = migration_key in applied_migrations - - self.stdout.write(f"Migration {migration_name}: {'āœ… Applied' if is_applied else 'āŒ Not Applied'}") - - # Check if columns exist in database - table_exists, columns = self.check_table_columns(app_label) - - if table_exists: - self.stdout.write(f"Database table exists with {len(columns)} columns:") - for col in sorted(columns): - self.stdout.write(f" - {col}") - else: - self.stdout.write("āŒ Database table does not exist") - - if check_only: - return - - # Fix strategy based on current state - if not is_applied and table_exists and 'data_file' in columns: - self.stdout.write("šŸ”§ Marking problematic migration as fake...") - try: - call_command('migrate', '--fake', app_label, migration_name.split('_')[0]) - self.stdout.write("āœ… Migration marked as fake") - except Exception as e: - self.stdout.write(f"āŒ Failed to fake migration: {e}") - - # Try to apply remaining migrations - self.stdout.write("šŸ”„ Applying remaining migrations...") - try: - call_command('migrate', app_label) - self.stdout.write("āœ… Migrations applied successfully") - except Exception as e: - self.stdout.write(f"āŒ Migration failed: {e}") - self.stdout.write("šŸ’” Try running: python manage.py reset_database --action migrations --confirm") - - def check_table_columns(self, app_label): - """Check what columns exist in the database table""" - table_map = { - 'data_analyzer': 'data_analyzer_requests', - 'weather_reporter': 'weather_reporter_weatheragentrequest', - 'job_posting_generator': 'job_posting_generator_jobpostingagentrequest', - 'social_ads_generator': 'social_ads_generator_socialadsagentrequest', - } - - table_name = table_map.get(app_label, f'{app_label}_request') - - try: - with connection.cursor() as cursor: - # PostgreSQL query to get column names - cursor.execute(""" - SELECT column_name - FROM information_schema.columns - WHERE table_name = %s - ORDER BY column_name - """, [table_name]) - - columns = [row[0] for row in cursor.fetchall()] - return True, columns - - except Exception as e: - # Try SQLite format - try: - with connection.cursor() as cursor: - cursor.execute(f"PRAGMA table_info({table_name})") - columns = [row[1] for row in cursor.fetchall()] # Column name is index 1 - return True, columns - except Exception: - return False, [] - - def show_migration_history(self, app_label): - """Show migration history for debugging""" - self.stdout.write(f"šŸ“œ Migration history for {app_label}:") - - recorder = MigrationRecorder(connection) - applied_migrations = recorder.applied_migrations() - - app_migrations = [m for m in applied_migrations if m[0] == app_label] - - if app_migrations: - for app, migration in sorted(app_migrations): - self.stdout.write(f" āœ… {migration}") - else: - self.stdout.write(f" No migrations applied for {app_label}") \ No newline at end of file diff --git a/agent_base/management/commands/populate_agents.py b/agent_base/management/commands/populate_agents.py deleted file mode 100644 index 551c00b..0000000 --- a/agent_base/management/commands/populate_agents.py +++ /dev/null @@ -1,127 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from agent_base.models import BaseAgent - -User = get_user_model() - - -class Command(BaseCommand): - help = 'Populate the database with default agents and create admin user' - - def add_arguments(self, parser): - parser.add_argument( - '--create-admin', - action='store_true', - help='Force create admin user even if superusers exist', - ) - - def handle(self, *args, **options): - self.stdout.write("Checking admin user...") - - # Only create admin if explicitly requested or no superusers exist - should_create_admin = options.get('create_admin', False) or not User.objects.filter(is_superuser=True).exists() - - if should_create_admin: - # Check if admin email already exists - admin_email = 'admin@quantumtaskai.com' - if User.objects.filter(email=admin_email).exists(): - self.stdout.write(f"Admin user with email {admin_email} already exists - skipping creation") - else: - User.objects.create_superuser( - username='admin', - email=admin_email, - password='P9cKE9G$R%ni#p', - first_name='Admin', - last_name='User' - ) - self.stdout.write("Created superuser: admin@quantumtaskai.com / P9cKE9G$R%ni#p") - else: - superuser_count = User.objects.filter(is_superuser=True).count() - self.stdout.write(f"Superuser(s) already exist ({superuser_count} found) - skipping admin creation") - - self.stdout.write("Creating default agents...") - - agents_data = [ - { - 'name': 'Weather Reporter', - 'slug': 'weather-reporter', - 'description': 'Get real-time weather information for any location worldwide. Provides current conditions, forecasts, and detailed weather reports.', - 'category': 'utilities', - 'price': 2.0, - 'icon': 'šŸŒ¤ļø', - 'agent_type': 'api', - }, - { - 'name': 'Data Analyzer', - 'slug': 'data-analyzer', - 'description': 'Analyze and extract insights from your data files. Supports PDF, CSV, and text analysis with AI-powered insights.', - 'category': 'analytics', - 'price': 5.0, - 'icon': 'šŸ“Š', - 'agent_type': 'webhook', - }, - { - 'name': 'Job Posting Generator', - 'slug': 'job-posting-generator', - 'description': 'Create professional job postings with AI assistance. Generate compelling job descriptions that attract the right candidates.', - 'category': 'content', - 'price': 3.0, - 'icon': 'šŸ’¼', - 'agent_type': 'webhook', - }, - { - 'name': 'Social Ads Generator', - 'slug': 'social-ads-generator', - 'description': 'Generate engaging social media advertisements. Create compelling ad copy for various platforms to boost your marketing campaigns.', - 'category': 'marketing', - 'price': 4.0, - 'icon': 'šŸ“±', - 'agent_type': 'webhook', - }, - { - 'name': '5 Whys Analysis Agent', - 'slug': 'five-whys-analyzer', - 'description': 'Systematic root cause analysis using the proven 5 Whys methodology to identify and solve business problems effectively.', - 'category': 'analytics', - 'price': 8.0, - 'icon': 'šŸ”', - 'agent_type': 'webhook', - }, - { - 'name': 'Email Writer', - 'slug': 'email-writer', - 'description': 'Generate professional emails for any purpose. Perfect for business communications, customer outreach, and personal correspondence.', - 'category': 'content', - 'price': 3.0, - 'icon': 'āœ‰ļø', - 'agent_type': 'api', - }, - ] - - created_count = 0 - updated_count = 0 - - for agent_data in agents_data: - agent, created = BaseAgent.objects.get_or_create( - slug=agent_data['slug'], - defaults=agent_data - ) - - if created: - created_count += 1 - self.stdout.write(f"Created: {agent.name}") - else: - # Update existing agent - for key, value in agent_data.items(): - if key != 'slug': - setattr(agent, key, value) - agent.save() - updated_count += 1 - self.stdout.write(f"Updated: {agent.name}") - - self.stdout.write( - self.style.SUCCESS( - f"Successfully processed {len(agents_data)} agents: " - f"{created_count} created, {updated_count} updated" - ) - ) \ No newline at end of file diff --git a/agent_base/management/commands/reset_database.py b/agent_base/management/commands/reset_database.py deleted file mode 100644 index c7f38ab..0000000 --- a/agent_base/management/commands/reset_database.py +++ /dev/null @@ -1,188 +0,0 @@ -from django.core.management.base import BaseCommand -from django.core.management import call_command -from django.db import connection, transaction -from django.conf import settings -import os -import shutil - - -class Command(BaseCommand): - help = 'Reset database and migrations for clean development/deployment' - - def add_arguments(self, parser): - parser.add_argument( - '--action', - choices=['migrations', 'database', 'full'], - default='full', - help='What to reset: migrations, database, or full (both)', - ) - parser.add_argument( - '--confirm', - action='store_true', - help='Confirm the destructive action', - ) - parser.add_argument( - '--keep-superuser', - action='store_true', - help='Keep existing superuser data during database reset', - ) - - def handle(self, *args, **options): - action = options['action'] - confirm = options['confirm'] - keep_superuser = options['keep_superuser'] - - if not confirm: - self.stdout.write( - self.style.WARNING( - "āš ļø This is a destructive operation! Add --confirm to proceed." - ) - ) - self.stdout.write("This will:") - if action in ['migrations', 'full']: - self.stdout.write(" - Delete all migration files") - if action in ['database', 'full']: - self.stdout.write(" - Drop all database tables") - self.stdout.write(" - Recreate fresh database") - return - - if action in ['migrations', 'full']: - self.reset_migrations() - - if action in ['database', 'full']: - self.reset_database(keep_superuser) - - if action == 'full': - self.create_fresh_migrations() - self.run_migrations() - if not keep_superuser: - self.create_initial_data() - - def reset_migrations(self): - """Delete all migration files except __init__.py""" - self.stdout.write("šŸ—‘ļø Deleting migration files...") - - apps_with_migrations = [ - 'agent_base', - 'authentication', - 'core', - 'wallet', - 'weather_reporter', - 'data_analyzer', - 'job_posting_generator', - 'social_ads_generator', - ] - - for app in apps_with_migrations: - migrations_dir = f"{app}/migrations" - if os.path.exists(migrations_dir): - # Keep __init__.py but delete all other migration files - for file in os.listdir(migrations_dir): - if file.endswith('.py') and file != '__init__.py': - file_path = os.path.join(migrations_dir, file) - os.remove(file_path) - self.stdout.write(f" Deleted: {file_path}") - - self.stdout.write(self.style.SUCCESS("āœ… Migration files deleted")) - - def reset_database(self, keep_superuser=False): - """Drop all tables and recreate database""" - self.stdout.write("šŸ—‘ļø Resetting database...") - - # Backup superuser if requested - superuser_data = None - if keep_superuser: - superuser_data = self.backup_superuser() - - # Get database engine - db_config = settings.DATABASES['default'] - engine = db_config['ENGINE'] - - if 'sqlite' in engine: - # For SQLite, just delete the file - db_file = db_config['NAME'] - if os.path.exists(db_file): - os.remove(db_file) - self.stdout.write(f" Deleted SQLite file: {db_file}") - - elif 'postgresql' in engine: - # For PostgreSQL, drop all tables - self.drop_all_postgresql_tables() - - else: - self.stdout.write( - self.style.ERROR(f"Unsupported database engine: {engine}") - ) - return - - self.stdout.write(self.style.SUCCESS("āœ… Database reset")) - - # Restore superuser if backed up - if superuser_data: - self.restore_superuser(superuser_data) - - def drop_all_postgresql_tables(self): - """Drop all tables in PostgreSQL database""" - with connection.cursor() as cursor: - # Get all table names - cursor.execute(""" - SELECT tablename FROM pg_tables - WHERE schemaname = 'public' - """) - tables = [row[0] for row in cursor.fetchall()] - - if tables: - # Drop all tables with CASCADE - tables_str = ', '.join(f'"{table}"' for table in tables) - cursor.execute(f'DROP TABLE IF EXISTS {tables_str} CASCADE') - self.stdout.write(f" Dropped {len(tables)} PostgreSQL tables") - - def backup_superuser(self): - """Backup superuser data before reset""" - try: - from django.contrib.auth import get_user_model - User = get_user_model() - - superuser = User.objects.filter(is_superuser=True).first() - if superuser: - return { - 'username': superuser.username, - 'email': superuser.email, - 'first_name': superuser.first_name, - 'last_name': superuser.last_name, - } - except Exception: - pass - return None - - def restore_superuser(self, superuser_data): - """Restore superuser after reset""" - if superuser_data: - self.stdout.write("šŸ”‘ Restoring superuser...") - call_command( - 'create_user', - superuser_data['email'], - 'admin123', # Default password - '--superuser', - '--username', superuser_data['username'], - '--first-name', superuser_data['first_name'], - '--last-name', superuser_data['last_name'], - ) - - def create_fresh_migrations(self): - """Create new migration files""" - self.stdout.write("šŸ“ Creating fresh migrations...") - call_command('makemigrations') - self.stdout.write(self.style.SUCCESS("āœ… Fresh migrations created")) - - def run_migrations(self): - """Apply all migrations""" - self.stdout.write("šŸ”„ Running migrations...") - call_command('migrate') - self.stdout.write(self.style.SUCCESS("āœ… Migrations applied")) - - def create_initial_data(self): - """Create initial data (agents and admin user)""" - self.stdout.write("šŸ‘¤ Creating initial data...") - call_command('populate_agents', '--create-admin') - self.stdout.write(self.style.SUCCESS("āœ… Initial data created")) \ No newline at end of file diff --git a/agent_base/management/commands/test_webhook.py b/agent_base/management/commands/test_webhook.py deleted file mode 100644 index 31c88fe..0000000 --- a/agent_base/management/commands/test_webhook.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.core.management.base import BaseCommand -from agent_base.processors import WebhookFormatDetector -import json - - -class Command(BaseCommand): - help = 'Test webhook format detection' - - def add_arguments(self, parser): - parser.add_argument('webhook_url', type=str, help='Webhook URL to test') - parser.add_argument('--timeout', type=int, default=10, help='Timeout in seconds') - parser.add_argument('--detect-best', action='store_true', help='Detect best format only') - - def handle(self, *args, **options): - webhook_url = options['webhook_url'] - timeout = options['timeout'] - - self.stdout.write(f"Testing webhook format for: {webhook_url}") - self.stdout.write("-" * 50) - - if options['detect_best']: - # Just detect the best format - best_format = WebhookFormatDetector.detect_best_format(webhook_url) - self.stdout.write(self.style.SUCCESS(f"Best format detected: {best_format}")) - else: - # Test all formats - results = WebhookFormatDetector.test_webhook_format(webhook_url, timeout) - - for result in results: - status = self.style.SUCCESS("āœ“") if result['success'] else self.style.ERROR("āœ—") - self.stdout.write(f"{status} {result['format']}") - self.stdout.write(f" Status Code: {result['status_code']}") - - if result['success']: - self.stdout.write(f" Response: {result['response'][:100]}...") - else: - self.stdout.write(f" Error: {result['error']}") - - self.stdout.write("") - - # Show best format recommendation - successful_formats = [r for r in results if r['success']] - if successful_formats: - best = successful_formats[0]['format'] - self.stdout.write(self.style.SUCCESS(f"Recommended format: {best}")) - else: - self.stdout.write(self.style.WARNING("No formats worked - webhook may be down")) - - self.stdout.write("-" * 50) - self.stdout.write("Format descriptions:") - self.stdout.write("• n8n_message: Standard N8N format with message object") - self.stdout.write("• direct_data: Direct data format with input field") - self.stdout.write("• simple: Simple key-value format") \ No newline at end of file diff --git a/agent_base/migrations/0001_initial.py b/agent_base/migrations/0001_initial.py deleted file mode 100644 index 0627cdd..0000000 --- a/agent_base/migrations/0001_initial.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-09 13:24 - -import uuid -from decimal import Decimal -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='BaseAgent', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200)), - ('slug', models.SlugField(unique=True)), - ('description', models.TextField()), - ('category', models.CharField(choices=[('analytics', 'Analytics'), ('utilities', 'Utilities'), ('content', 'Content'), ('marketing', 'Marketing'), ('customer-service', 'Customer Service')], max_length=50)), - ('price', models.DecimalField(decimal_places=2, max_digits=10)), - ('icon', models.CharField(default='šŸ¤–', max_length=100)), - ('is_active', models.BooleanField(default=True)), - ('rating', models.DecimalField(decimal_places=1, default=Decimal('4.5'), max_digits=3)), - ('review_count', models.IntegerField(default=0)), - ('agent_type', models.CharField(choices=[('webhook', 'Webhook'), ('api', 'API')], default='webhook', max_length=20)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'ordering': ['name'], - }, - ), - ] diff --git a/agent_base/migrations/__init__.py b/agent_base/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agent_base/models.py b/agent_base/models.py deleted file mode 100644 index 991db6a..0000000 --- a/agent_base/models.py +++ /dev/null @@ -1,90 +0,0 @@ -from django.db import models -from django.contrib.auth import get_user_model -from decimal import Decimal -import uuid - -User = get_user_model() - - -class BaseAgent(models.Model): - """Base model for all agents - used for catalog and marketplace""" - CATEGORIES = [ - ('analytics', 'Analytics'), - ('utilities', 'Utilities'), - ('content', 'Content'), - ('marketing', 'Marketing'), - ('customer-service', 'Customer Service'), - ] - - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(max_length=200) - slug = models.SlugField(unique=True) - description = models.TextField() - category = models.CharField(max_length=50, choices=CATEGORIES) - price = models.DecimalField(max_digits=10, decimal_places=2) - icon = models.CharField(max_length=100, default='šŸ¤–') - is_active = models.BooleanField(default=True) - rating = models.DecimalField(max_digits=3, decimal_places=1, default=Decimal('4.5')) - review_count = models.IntegerField(default=0) - agent_type = models.CharField(max_length=20, choices=[ - ('webhook', 'Webhook'), - ('api', 'API'), - ], default='webhook') - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ['name'] - - def __str__(self): - return self.name - - @property - def price_display(self): - return f"{self.price} AED" - - def get_gradient_class(self): - gradient_map = { - 'analytics': 'from-indigo-500 to-purple-600', - 'utilities': 'from-sky-400 to-blue-500', - 'content': 'from-purple-500 to-indigo-600', - 'marketing': 'from-pink-500 to-rose-600', - 'customer-service': 'from-blue-500 to-blue-600', - } - return gradient_map.get(self.category, 'from-gray-500 to-gray-600') - - def get_absolute_url(self): - """Get the URL for this agent's detail page""" - return f'/agents/{self.slug}/' - - -class BaseAgentRequest(models.Model): - """Base model for agent requests""" - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - user = models.ForeignKey(User, on_delete=models.CASCADE) - agent = models.ForeignKey(BaseAgent, on_delete=models.CASCADE) - status = models.CharField(max_length=20, choices=[ - ('pending', 'Pending'), - ('processing', 'Processing'), - ('completed', 'Completed'), - ('failed', 'Failed'), - ], default='pending') - cost = models.DecimalField(max_digits=10, decimal_places=2) - created_at = models.DateTimeField(auto_now_add=True) - processed_at = models.DateTimeField(null=True, blank=True) - - class Meta: - abstract = True - ordering = ['-created_at'] - - -class BaseAgentResponse(models.Model): - """Base model for agent responses""" - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - success = models.BooleanField(default=False) - error_message = models.TextField(blank=True) - processing_time = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - abstract = True \ No newline at end of file diff --git a/agent_base/processors.py b/agent_base/processors.py deleted file mode 100644 index 3a65ba8..0000000 --- a/agent_base/processors.py +++ /dev/null @@ -1,333 +0,0 @@ -import requests -from django.conf import settings -from django.utils import timezone -import json -import time -from abc import ABC, abstractmethod -from datetime import datetime - - -class BaseAgentProcessor(ABC): - """ - Base class for all agent processors. - - This class provides a standardized interface for processing agent requests, - whether they use webhooks or direct API calls. - """ - - # These should be set in subclasses - agent_slug = None - processor_type = None # 'webhook' or 'api' - - def __init__(self): - if not self.agent_slug: - raise ValueError("agent_slug must be defined in subclass") - if not self.processor_type: - raise ValueError("processor_type must be defined in subclass") - - @abstractmethod - def prepare_request_data(self, **kwargs): - """Prepare the request data for the webhook/API""" - pass - - @abstractmethod - def make_request(self, data, timeout=60): - """Make the actual HTTP request""" - pass - - @abstractmethod - def process_response(self, response_data, request_obj): - """Process the response and create database objects""" - pass - - def process_request(self, **kwargs): - """Main processing method - standardized across all agents""" - try: - # Prepare request data - request_data = self.prepare_request_data(**kwargs) - - # Make the request - response_data = self.make_request(request_data) - - # Create request object if provided - request_obj = kwargs.get('request_obj') - if request_obj: - # Process response and create response object - result = self.process_response(response_data, request_obj) - return result - else: - # Return raw response for testing - return response_data - - except Exception as e: - print(f"{self.agent_slug}: Error processing request: {e}") - if 'request_obj' in kwargs and kwargs['request_obj']: - kwargs['request_obj'].status = 'failed' - kwargs['request_obj'].save() - raise - - -class StandardWebhookProcessor(BaseAgentProcessor): - """ - Standardized webhook processor for N8N-based agents. - - This processor handles the common webhook format with message-based payload - and standardized response processing. - """ - - processor_type = 'webhook' - - # These should be set in subclasses - webhook_url = None - agent_id = None - - def __init__(self): - super().__init__() - if not self.webhook_url: - raise ValueError("webhook_url must be defined in subclass") - if not self.agent_id: - raise ValueError("agent_id must be defined in subclass") - - def prepare_message_text(self, **kwargs): - """Prepare the message text for the webhook - override in subclasses""" - return f"Process request for {self.agent_slug}" - - def prepare_request_data(self, **kwargs): - """Prepare standard webhook request data""" - user_id = kwargs.get('user_id') - - # Get the formatted message text - message_text = self.prepare_message_text(**kwargs) - - return { - 'message': { - 'text': message_text - }, - 'sessionId': f'{self.agent_slug}_{int(datetime.now().timestamp() * 1000)}', - 'userId': str(user_id), - 'agentId': str(self.agent_id), - **self.get_additional_fields(**kwargs) - } - - def get_additional_fields(self, **kwargs): - """Get additional fields for the webhook payload - override in subclasses""" - return {} - - def make_request(self, data, timeout=60): - """Make webhook request with standardized error handling""" - try: - print(f"{self.agent_slug}: Sending webhook request to {self.webhook_url}") - print(f"{self.agent_slug}: Payload: {json.dumps(data, indent=2)}") - - start_time = time.time() - response = requests.post(self.webhook_url, json=data, timeout=timeout) - processing_time = time.time() - start_time - - print(f"{self.agent_slug}: Response status: {response.status_code}") - print(f"{self.agent_slug}: Response text: {response.text[:500]}...") - - response.raise_for_status() - - # Check if response has content - if not response.text.strip(): - raise ValueError("Empty response from webhook") - - # Try to parse JSON, fallback to text - try: - response_data = response.json() - except ValueError: - response_data = {'output': response.text} - - # Add processing metadata - response_data['processing_time'] = processing_time - response_data['success'] = True - - return response_data - - except requests.exceptions.RequestException as e: - print(f"{self.agent_slug}: Webhook request error: {e}") - raise ValueError(f"Webhook error: {e}") - except Exception as e: - print(f"{self.agent_slug}: Unexpected error: {e}") - raise ValueError(f"Processing error: {e}") - - -class StandardAPIProcessor(BaseAgentProcessor): - """ - Standardized API processor for direct API integrations. - - This processor handles direct API calls with authentication and - standardized response processing. - """ - - processor_type = 'api' - - # These should be set in subclasses - api_base_url = None - api_key_env = None - auth_method = 'bearer' # 'bearer', 'api-key', 'basic', 'query' - - def __init__(self): - super().__init__() - if not self.api_base_url: - raise ValueError("api_base_url must be defined in subclass") - if self.api_key_env and hasattr(settings, self.api_key_env): - self.api_key = getattr(settings, self.api_key_env) - else: - self.api_key = None - - def get_headers(self): - """Get headers for API request""" - headers = {'Content-Type': 'application/json'} - - if self.api_key: - if self.auth_method == 'bearer': - headers['Authorization'] = f'Bearer {self.api_key}' - elif self.auth_method == 'api-key': - headers['X-API-Key'] = self.api_key - elif self.auth_method == 'basic': - import base64 - auth_string = base64.b64encode(f'{self.api_key}:'.encode()).decode() - headers['Authorization'] = f'Basic {auth_string}' - - return headers - - def get_endpoint(self, **kwargs): - """Get the API endpoint - override in subclasses""" - return self.api_base_url - - def prepare_request_data(self, **kwargs): - """Prepare API request data - override in subclasses""" - return kwargs - - def make_request(self, data, timeout=60): - """Make API request with standardized error handling""" - try: - endpoint = self.get_endpoint(**data) - headers = self.get_headers() - - # For query-based auth, add API key to URL - if self.auth_method == 'query' and self.api_key: - separator = '&' if '?' in endpoint else '?' - endpoint = f"{endpoint}{separator}appid={self.api_key}" - - print(f"{self.agent_slug}: Making API request to {endpoint}") - print(f"{self.agent_slug}: Headers: {headers}") - print(f"{self.agent_slug}: Data: {json.dumps(data, indent=2)}") - - start_time = time.time() - - # Use GET for most API calls, POST for data submission - if self.should_use_get(**data): - response = requests.get(endpoint, headers=headers, timeout=timeout) - else: - response = requests.post(endpoint, json=data, headers=headers, timeout=timeout) - - processing_time = time.time() - start_time - - print(f"{self.agent_slug}: Response status: {response.status_code}") - print(f"{self.agent_slug}: Response text: {response.text[:500]}...") - - response.raise_for_status() - - # Try to parse JSON - try: - response_data = response.json() - except ValueError: - response_data = {'result': response.text} - - # Add processing metadata - response_data['processing_time'] = processing_time - response_data['success'] = True - - return response_data - - except requests.exceptions.RequestException as e: - print(f"{self.agent_slug}: API request error: {e}") - raise ValueError(f"API error: {e}") - except Exception as e: - print(f"{self.agent_slug}: Unexpected error: {e}") - raise ValueError(f"Processing error: {e}") - - def should_use_get(self, **kwargs): - """Determine if GET should be used instead of POST - override in subclasses""" - return True - - -class WebhookFormatDetector: - """ - Utility class to detect webhook format by testing endpoints. - - This helps determine what format a webhook expects by sending - test requests and analyzing the response. - """ - - @staticmethod - def test_webhook_format(webhook_url, timeout=10): - """Test webhook to determine expected format""" - test_formats = [ - # N8N message format - { - 'name': 'n8n_message', - 'payload': { - 'message': {'text': 'Test message'}, - 'sessionId': 'test_session', - 'userId': 'test_user', - 'agentId': '1' - } - }, - # Direct data format - { - 'name': 'direct_data', - 'payload': { - 'input': 'test data', - 'user_id': 'test_user', - 'agent_type': 'test_agent' - } - }, - # Simple format - { - 'name': 'simple', - 'payload': {'test': 'data'} - } - ] - - results = [] - - for format_test in test_formats: - try: - response = requests.post( - webhook_url, - json=format_test['payload'], - timeout=timeout - ) - results.append({ - 'format': format_test['name'], - 'status_code': response.status_code, - 'success': response.status_code == 200, - 'response': response.text[:200], - 'error': None - }) - except Exception as e: - results.append({ - 'format': format_test['name'], - 'status_code': None, - 'success': False, - 'response': None, - 'error': str(e) - }) - - return results - - @staticmethod - def detect_best_format(webhook_url): - """Detect the best format for a webhook""" - results = WebhookFormatDetector.test_webhook_format(webhook_url) - - # Find the first successful format - for result in results: - if result['success']: - return result['format'] - - # If no format works, return the first one (n8n_message) as default - return 'n8n_message' \ No newline at end of file diff --git a/agent_base/urls.py b/agent_base/urls.py deleted file mode 100644 index 202d015..0000000 --- a/agent_base/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'agent_base' - -urlpatterns = [ - path('marketplace/', views.marketplace_view, name='marketplace'), - path('api/agents/', views.agents_api_view, name='agents_api'), -] \ No newline at end of file diff --git a/agent_base/views.py b/agent_base/views.py deleted file mode 100644 index 146c878..0000000 --- a/agent_base/views.py +++ /dev/null @@ -1,160 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.http import JsonResponse -from django.db.models import Q -from django_ratelimit.decorators import ratelimit -from django_ratelimit import UNSAFE -from .models import BaseAgent -import logging - -logger = logging.getLogger('agent_base.security') - - -@ratelimit(key='ip', rate='60/m', method='GET', block=False) -def marketplace_view(request): - """Professional marketplace view with agent system - Rate limited to 60 requests per minute per IP""" - # Check if rate limited - if getattr(request, 'limited', False): - logger.warning(f"Marketplace rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}") - messages.error(request, 'Too many requests. Please wait a moment before refreshing.') - # Still show marketplace but with warning - - # Get all agents for marketplace with optimized query - agents_queryset = BaseAgent.objects.filter(is_active=True).select_related().order_by('category', 'name') - - # Server-side search with validation - search_query = request.GET.get('search', '').strip() - if search_query: - # Validate search query (max length and safe characters) - if len(search_query) > 100: - logger.warning(f"Search query too long: {len(search_query)} characters") - messages.error(request, 'Search query too long. Please keep it under 100 characters.') - search_query = search_query[:100] - - # Remove potential SQL injection patterns and sanitize - import re - search_query = re.sub(r'[^\w\s\-\.]', '', search_query) - - if search_query: - agents_queryset = agents_queryset.filter( - Q(name__icontains=search_query) | - Q(description__icontains=search_query) - ) - logger.info(f"Marketplace search performed: '{search_query}'") - - # Filter by category if specified with validation - category = request.GET.get('category') - if category: - # Validate category against allowed choices - valid_categories = [choice[0] for choice in BaseAgent.CATEGORIES] - if category in valid_categories: - agents_queryset = agents_queryset.filter(category=category) - logger.info(f"Marketplace filtered by valid category: {category}") - else: - logger.warning(f"Invalid category parameter attempted: {category}") - category = None # Reset to show all agents - - # Get agents and categories in single query - agents = list(agents_queryset) - categories = BaseAgent.objects.filter(is_active=True).values_list('category', 'category').distinct() - - context = { - 'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0, - 'agents': agents, - 'categories': categories, - 'selected_category': category, - 'search_query': search_query if 'search_query' in locals() else '', - } - - return render(request, 'agent_base/marketplace.html', context) - - - -@ratelimit(key='ip', rate='30/m', method='GET', block=False) -def agents_api_view(request): - """API endpoint for agents list - Rate limited to 30 requests per minute per IP""" - # Check if rate limited - if getattr(request, 'limited', False): - logger.warning(f"Agents API rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}") - return JsonResponse({ - 'error': 'Rate limit exceeded. Please try again later.', - 'agents': [], - 'total_count': 0, - }, status=429) - - agents = BaseAgent.objects.filter(is_active=True) - - # Server-side search with validation for API - search_query = request.GET.get('search', '').strip() - if search_query: - # Validate search query (max length and safe characters) - if len(search_query) > 100: - logger.warning(f"API search query too long: {len(search_query)} characters") - return JsonResponse({ - 'error': 'Search query too long. Maximum 100 characters allowed.', - 'agents': [], - 'total_count': 0, - }, status=400) - - # Remove potential SQL injection patterns and sanitize - import re - search_query = re.sub(r'[^\w\s\-\.]', '', search_query) - - if search_query: - agents = agents.filter( - Q(name__icontains=search_query) | - Q(description__icontains=search_query) - ) - logger.info(f"API search performed: '{search_query}'") - - # Filter by category if specified with validation - category = request.GET.get('category') - if category: - # Validate category against allowed choices - valid_categories = [choice[0] for choice in BaseAgent.CATEGORIES] - if category in valid_categories: - agents = agents.filter(category=category) - logger.info(f"API filtered by valid category: {category}") - else: - logger.warning(f"Invalid category parameter in API: {category}") - return JsonResponse({ - 'error': 'Invalid category parameter', - 'valid_categories': valid_categories, - 'agents': [], - 'total_count': 0, - }, status=400) - - # Add pagination for security (limit large responses) with validation - try: - page_size = min(int(request.GET.get('limit', 50)), 100) # Max 100 agents per request - offset = max(int(request.GET.get('offset', 0)), 0) - except (ValueError, TypeError): - logger.warning(f"Invalid pagination parameters in API request") - return JsonResponse({ - 'error': 'Invalid pagination parameters. Limit and offset must be integers.', - 'agents': [], - 'total_count': 0, - }, status=400) - - agents_page = agents[offset:offset + page_size] - - # Only return essential data (minimize information disclosure) - agents_data = [] - for agent in agents_page: - agents_data.append({ - 'name': agent.name, - 'slug': agent.slug, - 'description': agent.description[:200], # Limit description length - 'category': agent.category, - 'price': float(agent.price), - 'icon': agent.icon, - 'rating': float(agent.rating), - }) - - return JsonResponse({ - 'agents': agents_data, - 'total_count': agents.count(), - 'returned_count': len(agents_data), - 'offset': offset, - 'limit': page_size, - }) \ No newline at end of file diff --git a/core/views.py b/core/views.py index 45ffc2f..473884e 100644 --- a/core/views.py +++ b/core/views.py @@ -6,7 +6,7 @@ from django.core.mail import send_mail from django.conf import settings from django_ratelimit.decorators import ratelimit from django_ratelimit import UNSAFE -from agent_base.models import BaseAgent +from workflows.config.agents import get_all_agents from .models import ContactSubmission from django.db import connection import logging @@ -23,8 +23,9 @@ def homepage_view(request): messages.warning(request, 'Too many requests. Please wait a moment before refreshing.') try: - # Get featured agents for homepage with safe querying - featured_agents = BaseAgent.objects.filter(is_active=True).order_by('name')[:6] + # Get featured agents for homepage from config + all_agents = get_all_agents() + featured_agents = list(all_agents.items())[:6] context = { 'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0, @@ -50,8 +51,9 @@ def pricing_view(request): return redirect('wallet:wallet_topup') try: - # Get sample agents to show pricing context with safe querying - sample_agents = BaseAgent.objects.filter(is_active=True).order_by('name')[:4] + # Get sample agents to show pricing context from config + all_agents = get_all_agents() + sample_agents = list(all_agents.items())[:4] context = { 'sample_agents': sample_agents, @@ -242,9 +244,9 @@ def health_check_view(request): 'response_time_ms': round((time.time() - start_time) * 1000, 2) } - # If database is working, try to get agent count + # If database is working, get agent count from config try: - agent_count = BaseAgent.objects.filter(is_active=True).count() + agent_count = len(get_all_agents()) health_data['checks']['agents'] = { 'status': 'healthy', 'active_count': agent_count @@ -252,7 +254,7 @@ def health_check_view(request): except Exception as e: health_data['checks']['agents'] = { 'status': 'warning', - 'error': 'Could not query agents', + 'error': 'Could not load agent config', 'message': str(e)[:100] } diff --git a/data_analyzer/__init__.py b/data_analyzer/__init__.py deleted file mode 100644 index 69dfc55..0000000 --- a/data_analyzer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Data Analysis Agent Agent App \ No newline at end of file diff --git a/data_analyzer/admin.py b/data_analyzer/admin.py deleted file mode 100644 index 5903f60..0000000 --- a/data_analyzer/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin -from .models import DataAnalysisAgentRequest, DataAnalysisAgentResponse - - -@admin.register(DataAnalysisAgentRequest) -class DataAnalysisAgentRequestAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'status', 'created_at', 'cost'] - list_filter = ['status', 'created_at'] - search_fields = ['user__email', 'user__username'] - readonly_fields = ['id', 'created_at', 'processed_at'] - ordering = ['-created_at'] - - -@admin.register(DataAnalysisAgentResponse) -class DataAnalysisAgentResponseAdmin(admin.ModelAdmin): - list_display = ['id', 'request', 'success', 'created_at'] - list_filter = ['success', 'created_at'] - readonly_fields = ['id', 'created_at'] - ordering = ['-created_at'] \ No newline at end of file diff --git a/data_analyzer/apps.py b/data_analyzer/apps.py deleted file mode 100644 index ec87c6f..0000000 --- a/data_analyzer/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class DataAnalysisAgentConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'data_analyzer' \ No newline at end of file diff --git a/data_analyzer/management/__init__.py b/data_analyzer/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/data_analyzer/management/commands/__init__.py b/data_analyzer/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/data_analyzer/management/commands/cleanup_uploads.py b/data_analyzer/management/commands/cleanup_uploads.py deleted file mode 100644 index 648b202..0000000 --- a/data_analyzer/management/commands/cleanup_uploads.py +++ /dev/null @@ -1,125 +0,0 @@ -from django.core.management.base import BaseCommand -from django.utils import timezone -from datetime import timedelta -from data_analyzer.models import DataAnalysisAgentRequest -import os -import glob - - -class Command(BaseCommand): - help = 'Clean up old uploaded files from data analyzer' - - def add_arguments(self, parser): - parser.add_argument( - '--age-hours', - type=int, - default=24, - help='Delete files older than this many hours (default: 24)' - ) - parser.add_argument( - '--dry-run', - action='store_true', - help='Show what would be deleted without actually deleting' - ) - parser.add_argument( - '--force-orphaned', - action='store_true', - help='Also delete orphaned files not associated with database records' - ) - - def handle(self, *args, **options): - age_hours = options['age_hours'] - dry_run = options['dry_run'] - force_orphaned = options['force_orphaned'] - - cutoff_time = timezone.now() - timedelta(hours=age_hours) - - self.stdout.write(f"Looking for files older than {age_hours} hours ({cutoff_time})") - - if dry_run: - self.stdout.write(self.style.WARNING("DRY RUN MODE - No files will be deleted")) - - # Clean up files associated with old database records - old_requests = DataAnalysisAgentRequest.objects.filter( - created_at__lt=cutoff_time - ) - - deleted_count = 0 - error_count = 0 - - for request in old_requests: - if request.data_file: - try: - file_path = request.data_file.path - if os.path.exists(file_path): - if not dry_run: - os.remove(file_path) - self.stdout.write(f"Deleted: {file_path}") - else: - self.stdout.write(f"Would delete: {file_path}") - deleted_count += 1 - else: - self.stdout.write(f"File already gone: {file_path}") - except Exception as e: - self.stdout.write( - self.style.ERROR(f"Error deleting {request.data_file.path}: {e}") - ) - error_count += 1 - - # Clean up orphaned files if requested - if force_orphaned: - self.stdout.write("Checking for orphaned files...") - - try: - from django.conf import settings - upload_path = os.path.join(settings.MEDIA_ROOT, 'uploads/data_analyzer/') - - if os.path.exists(upload_path): - # Get all files in upload directory - all_files = glob.glob(os.path.join(upload_path, '*')) - - # Get all files currently referenced in database - db_files = set() - for request in DataAnalysisAgentRequest.objects.filter(data_file__isnull=False): - if request.data_file: - try: - db_files.add(request.data_file.path) - except: - pass - - # Find orphaned files - for file_path in all_files: - if os.path.isfile(file_path) and file_path not in db_files: - file_age = timezone.now() - timezone.datetime.fromtimestamp( - os.path.getctime(file_path), - tz=timezone.get_current_timezone() - ) - - if file_age > timedelta(hours=age_hours): - if not dry_run: - os.remove(file_path) - self.stdout.write(f"Deleted orphaned file: {file_path}") - else: - self.stdout.write(f"Would delete orphaned file: {file_path}") - deleted_count += 1 - - except Exception as e: - self.stdout.write( - self.style.ERROR(f"Error checking orphaned files: {e}") - ) - error_count += 1 - - # Summary - if dry_run: - self.stdout.write( - self.style.SUCCESS(f"DRY RUN: Would delete {deleted_count} files") - ) - else: - self.stdout.write( - self.style.SUCCESS(f"Successfully deleted {deleted_count} files") - ) - - if error_count > 0: - self.stdout.write( - self.style.ERROR(f"Encountered {error_count} errors") - ) \ No newline at end of file diff --git a/data_analyzer/migrations/0001_initial.py b/data_analyzer/migrations/0001_initial.py deleted file mode 100644 index bf8cfa2..0000000 --- a/data_analyzer/migrations/0001_initial.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-10 04:09 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('agent_base', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='DataAnalysisAgentRequest', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)), - ('cost', models.DecimalField(decimal_places=2, max_digits=10)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('processed_at', models.DateTimeField(blank=True, null=True)), - ('data_file', models.FileField(blank=True, upload_to='uploads/data_analyzer/')), - ('analysis_type', models.CharField(choices=[('summary', 'Summary Analysis'), ('detailed', 'Detailed Analysis'), ('statistical', 'Statistical Analysis')], default='summary', max_length=50)), - ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agent_base.baseagent')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Data Analysis Agent Request', - 'verbose_name_plural': 'Data Analysis Agent Requests', - 'db_table': 'data_analyzer_requests', - }, - ), - migrations.CreateModel( - name='DataAnalysisAgentResponse', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('success', models.BooleanField(default=False)), - ('error_message', models.TextField(blank=True)), - ('processing_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('analysis_results', models.JSONField(blank=True, default=dict)), - ('insights_summary', models.TextField(blank=True)), - ('report_text', models.TextField(blank=True)), - ('raw_response', models.JSONField(blank=True, default=dict)), - ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='response', to='data_analyzer.dataanalysisagentrequest')), - ], - options={ - 'verbose_name': 'Data Analysis Agent Response', - 'verbose_name_plural': 'Data Analysis Agent Responses', - 'db_table': 'data_analyzer_responses', - }, - ), - ] diff --git a/data_analyzer/migrations/0002_auto_20250710_0431.py b/data_analyzer/migrations/0002_auto_20250710_0431.py deleted file mode 100644 index 050f6ee..0000000 --- a/data_analyzer/migrations/0002_auto_20250710_0431.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-10 04:31 -# Modified to prevent duplicate column errors - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('data_analyzer', '0001_initial'), - ] - - operations = [ - # No operations - fields already exist in database - # This prevents "column already exists" errors during deployment - ] \ No newline at end of file diff --git a/data_analyzer/migrations/0003_dataanalysisagentrequest_input_text_and_more.py b/data_analyzer/migrations/0003_dataanalysisagentrequest_input_text_and_more.py deleted file mode 100644 index 20d1bb3..0000000 --- a/data_analyzer/migrations/0003_dataanalysisagentrequest_input_text_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-10 04:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('data_analyzer', '0002_auto_20250710_0431'), - ] - - operations = [ - migrations.AddField( - model_name='dataanalysisagentrequest', - name='input_text', - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name='dataanalysisagentresponse', - name='output_text', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/data_analyzer/migrations/0004_fix_duplicate_fields.py b/data_analyzer/migrations/0004_fix_duplicate_fields.py deleted file mode 100644 index 32f4be2..0000000 --- a/data_analyzer/migrations/0004_fix_duplicate_fields.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated manually to fix duplicate field migration errors - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('data_analyzer', '0003_dataanalysisagentrequest_input_text_and_more'), - ] - - operations = [ - # This migration exists to mark the problematic fields as "already applied" - # It doesn't actually change anything, just syncs Django's migration state - # with the actual database schema - - # The following fields already exist in the database but Django thinks they need to be added: - # - data_file (from 0002_auto_20250710_0431) - # - analysis_type (from 0002_auto_20250710_0431) - # - analysis_results (from 0002_auto_20250710_0431) - # - insights_summary (from 0002_auto_20250710_0431) - # - report_text (from 0002_auto_20250710_0431) - - # This empty migration helps sync the state without actually changing the database - ] \ No newline at end of file diff --git a/data_analyzer/migrations/0005_alter_dataanalysisagentrequest_data_file.py b/data_analyzer/migrations/0005_alter_dataanalysisagentrequest_data_file.py deleted file mode 100644 index c27226c..0000000 --- a/data_analyzer/migrations/0005_alter_dataanalysisagentrequest_data_file.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-27 03:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("data_analyzer", "0004_fix_duplicate_fields"), - ] - - operations = [ - migrations.AlterField( - model_name="dataanalysisagentrequest", - name="data_file", - field=models.FileField( - blank=True, - help_text="PDF file for analysis", - upload_to="uploads/data_analyzer/", - ), - ), - ] diff --git a/data_analyzer/migrations/__init__.py b/data_analyzer/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/data_analyzer/models.py b/data_analyzer/models.py deleted file mode 100644 index efc23c5..0000000 --- a/data_analyzer/models.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.db import models -from decimal import Decimal -from agent_base.models import BaseAgentRequest, BaseAgentResponse -from django.db.models.signals import post_delete -from django.dispatch import receiver -import os - - -class DataAnalysisAgentRequest(BaseAgentRequest): - """Data Analysis Agent request tracking""" - - # Agent-specific request fields - data_file = models.FileField( - upload_to='uploads/data_analyzer/', - blank=True, - help_text='PDF file for analysis' - ) - analysis_type = models.CharField( - max_length=50, - choices=[ - ('summary', 'Summary Analysis'), - ('detailed', 'Detailed Analysis'), - ('statistical', 'Statistical Analysis'), - ], - default='summary' - ) - # Legacy field (keeping for compatibility) - input_text = models.TextField(blank=True, null=True) - - - def delete(self, *args, **kwargs): - """Custom delete method to clean up uploaded file""" - # Delete the file before deleting the database record - if self.data_file: - try: - if os.path.exists(self.data_file.path): - os.remove(self.data_file.path) - print(f"Deleted file during model deletion: {self.data_file.path}") - except Exception as e: - print(f"Warning - Failed to delete file during model deletion: {e}") - - # Call the parent delete method - super().delete(*args, **kwargs) - - class Meta: - db_table = 'data_analyzer_requests' - verbose_name = 'Data Analysis Agent Request' - verbose_name_plural = 'Data Analysis Agent Requests' - - -class DataAnalysisAgentResponse(BaseAgentResponse): - """Data Analysis Agent response storage""" - - request = models.OneToOneField( - DataAnalysisAgentRequest, - on_delete=models.CASCADE, - related_name='response' - ) - - # Agent-specific response fields - analysis_results = models.JSONField(default=dict, blank=True) - insights_summary = models.TextField(blank=True) - report_text = models.TextField(blank=True) - raw_response = models.JSONField(default=dict, blank=True) - # Legacy field (keeping for compatibility) - output_text = models.TextField(blank=True, null=True) - - - class Meta: - db_table = 'data_analyzer_responses' - verbose_name = 'Data Analysis Agent Response' - verbose_name_plural = 'Data Analysis Agent Responses' - - -@receiver(post_delete, sender=DataAnalysisAgentRequest) -def cleanup_data_file(sender, instance, **kwargs): - """Signal handler to ensure uploaded files are deleted when request is deleted""" - if instance.data_file: - try: - if os.path.exists(instance.data_file.path): - os.remove(instance.data_file.path) - print(f"Signal cleanup: Deleted file {instance.data_file.path}") - except Exception as e: - print(f"Signal cleanup warning - Failed to delete file: {e}") \ No newline at end of file diff --git a/data_analyzer/n8n_workflows/README.md b/data_analyzer/n8n_workflows/README.md deleted file mode 100644 index e706695..0000000 --- a/data_analyzer/n8n_workflows/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Data Analyzer Agent - N8N Workflow - -## Overview -This directory contains the N8N workflow configuration for the Data Analyzer Agent, which processes uploaded files (CSV, Excel, PDF) and provides intelligent data analysis. - -## Workflow Files -- `workflow.json` - Production workflow for N8N import -- `workflow_dev.json` - Development/testing version (optional) -- `workflow_backup.json` - Backup version for disaster recovery - -## Webhook Configuration -- **Webhook URL**: Configured via `N8N_WEBHOOK_DATA_ANALYZER` environment variable -- **HTTP Method**: POST -- **Expected Data Format**: - ```json - { - "file_name": "data.csv", - "file_content": "base64_encoded_content", - "analysis_type": "statistical", - "user_request": "Analyze sales trends" - } - ``` - -## Setup Instructions - -### 1. Import Workflow to N8N -1. Open your N8N instance -2. Click "Import from File" or "Import from URL" -3. Upload the `workflow.json` file -4. Configure credentials (OpenAI API key, etc.) -5. Activate the workflow - -### 2. Configure Webhook URL -1. Copy the webhook URL from N8N -2. Set environment variable: `N8N_WEBHOOK_DATA_ANALYZER=https://your-n8n.com/webhook/data-analyzer` -3. Restart your Django application - -### 3. Test the Workflow -```bash -# Test via Django application -python manage.py test_webhook data_analyzer - -# Or test directly via curl -curl -X POST https://your-n8n.com/webhook/data-analyzer \ - -H "Content-Type: application/json" \ - -d '{"file_name":"test.csv","file_content":"dGVzdA==","analysis_type":"basic"}' -``` - -## Workflow Components -- **Webhook Node**: Receives requests from Django application -- **AI Processing**: Uses OpenAI GPT-4 for data analysis -- **Response Node**: Returns structured analysis results -- **Error Handling**: Manages failures and timeouts - -## Expected Response Format -```json -{ - "success": true, - "analysis": { - "summary": "Data analysis summary", - "insights": ["Key insight 1", "Key insight 2"], - "recommendations": ["Recommendation 1", "Recommendation 2"], - "charts": [{"type": "bar", "data": {...}}] - }, - "processing_time": 1.5 -} -``` - -## Troubleshooting -- **Webhook not responding**: Check N8N workflow is active and URL is correct -- **Authentication errors**: Verify OpenAI API credentials in N8N -- **Timeout issues**: Increase workflow timeout settings for large files -- **Rate limiting**: Monitor OpenAI API usage limits - -## Maintenance -- Regularly backup workflow configurations -- Monitor workflow execution logs in N8N -- Update AI prompts based on user feedback -- Scale webhook handling based on usage patterns \ No newline at end of file diff --git a/data_analyzer/n8n_workflows/pdf_data_analyzer.json b/data_analyzer/n8n_workflows/pdf_data_analyzer.json deleted file mode 100644 index 3545514..0000000 --- a/data_analyzer/n8n_workflows/pdf_data_analyzer.json +++ /dev/null @@ -1,316 +0,0 @@ -{ - "name": "pdf_data_analyzer", - "nodes": [ - { - "parameters": { - "content": "## Error Handling\n\nIf processing fails, the workflow will return an error response with details about what went wrong.", - "height": 120, - "width": 280 - }, - "id": "03452a38-11bc-40e4-abfd-66a3b2d28d10", - "name": "Error Info", - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 560, - 2840 - ] - }, - { - "parameters": { - "jsCode": "// Handle any errors that occur during processing\nconst error = $input.item(0).json.error || 'Unknown error occurred';\n\nreturn {\n json: {\n status: 'error',\n error_message: error,\n timestamp: new Date().toISOString(),\n help: 'Make sure you are uploading a valid PDF file using the \"file\" form field'\n }\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - -1000, - 2360 - ], - "id": "9888231c-5de1-4160-a53c-a951ca30417d", - "name": "Error Handler" - }, - { - "parameters": { - "content": "## Simple PDF Processor\n\n**Purpose:** Upload PDF → Extract Text → AI Analysis → JSON Response\n\n**Usage:**\n```bash\ncurl -X POST https://your-n8n.com/webhook/simple-pdf-processor \\\n -F \"file=@document.pdf\"\n```\n\n**Response:** AI analysis of PDF content in JSON format", - "height": 280, - "width": 350 - }, - "id": "cb3831b1-8b8f-4726-991f-0de535bbdc9c", - "name": "Workflow Overview1", - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - -740, - 2500 - ] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{$('Error Handler').item.json}}", - "options": {} - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - -780, - 2360 - ], - "id": "6603a971-fb15-41a3-b5b9-001bb13305ad", - "name": "Return Error Response1" - }, - { - "parameters": { - "jsCode": "// Simple PDF file preparation\nconst items = $input.all();\n\nif (!items || items.length === 0) {\n throw new Error('No input data received');\n}\n\nconst item = items[0];\nconsole.log('Processing PDF upload...');\n\n// Check if we have binary data\nif (!item.binary || !item.binary.file) {\n throw new Error('No PDF file found in upload. Make sure to use \"file\" as the form field name.');\n}\n\nconst fileData = item.binary.file;\nconst fileName = fileData.fileName || 'uploaded.pdf';\nconst fileSize = fileData.fileSize || 0;\n\nconsole.log(`File: ${fileName}, Size: ${fileSize} bytes`);\n\n// Prepare data for PDF extraction\nreturn {\n json: {\n filename: fileName,\n fileSize: fileSize,\n uploadedAt: new Date().toISOString(),\n status: 'ready_for_processing'\n },\n binary: {\n // Use the key expected by extractFromFile node\n 'pdf_file': fileData\n }\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 100, - 2040 - ], - "id": "93f3dc69-190e-4c32-8175-e9d098873e8e", - "name": "Prepare PDF Data" - }, - { - "parameters": { - "jsCode": "// Ultra-simple n8n formatting code\nconst items = $input.all();\nconst text = items[0].json.text;\n\n// Split by headings and format\nconst sections = text.split('### ').filter(part => part.trim());\n\nconst formatted = sections.map(section => {\n const lines = section.trim().split('\\n');\n const heading = lines[0];\n const content = lines.slice(1).join('\\n');\n \n return {\n heading: heading,\n content: content\n };\n});\n\nreturn [{\n json: {\n sections: formatted,\n timestamp: new Date().toISOString()\n }\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 916, - 2040 - ], - "id": "5b160fd8-0a8b-4494-9935-7d9bf7db880f", - "name": "Format Response" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{$('Format Response').item.json}}", - "options": { - "responseHeaders": { - "entries": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - } - } - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - 1136, - 2040 - ], - "id": "1bc45e3e-b86b-4a7d-9970-e8464d09f9a1", - "name": "Return JSON Response" - }, - { - "parameters": { - "httpMethod": "POST", - "path": "simple-pdf-processor", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2, - "position": [ - -120, - 2040 - ], - "id": "c380ce52-58c5-4c38-946b-7e86a2c645c3", - "name": "PDF Upload Webhook1", - "webhookId": "simple-pdf-processor" - }, - { - "parameters": { - "operation": "pdf", - "binaryPropertyName": "pdf_file", - "options": {} - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1, - "position": [ - 320, - 2040 - ], - "id": "bcd3c33b-5376-43b6-9312-3570fb2799ca", - "name": "Extract PDF Text1" - }, - { - "parameters": { - "promptType": "define", - "text": "={{ $json.text }}", - "messages": { - "messageValues": [ - { - "type": "AIMessagePromptTemplate", - "message": "You are a helpful document analysis assistant. Analyze the provided PDF text content and provide useful insights." - }, - { - "message": "Please analyze this PDF document and provide:\n\n1. **Summary**: A brief overview of the document content\n2. **Key Points**: Main topics or important information found\n3. **Document Type**: What type of document this appears to be\n4. **Insights**: Any notable findings or analysis\n\nDocument text to analyze:\n{{ $json.text }}\n\nPlease provide your analysis in a clear, structured format." - } - ] - }, - "batching": {} - }, - "type": "@n8n/n8n-nodes-langchain.chainLlm", - "typeVersion": 1.7, - "position": [ - 540, - 2040 - ], - "id": "76ac82c9-1843-4ebe-98f7-bdd9b45d3610", - "name": "AI Document Analyzer1" - }, - { - "parameters": { - "model": "llama-3.3-70b-versatile", - "options": { - "maxTokensToSample": 2000, - "temperature": 0.3 - } - }, - "type": "@n8n/n8n-nodes-langchain.lmChatGroq", - "typeVersion": 1, - "position": [ - 628, - 2260 - ], - "id": "d3fce174-1ef1-4b0f-84f3-477c49a80840", - "name": "Groq Chat Model1", - "credentials": { - "groqApi": { - "id": "9HviwDANITBPqb1I", - "name": "Groq account" - } - } - }, - { - "parameters": { - "formTitle": "FIle Upload", - "formFields": { - "values": [ - { - "fieldLabel": "file", - "fieldType": "file", - "multipleFiles": false - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.formTrigger", - "typeVersion": 2.2, - "position": [ - -120, - 2400 - ], - "id": "de020e99-cde6-4575-afb0-c568fe0e5d63", - "name": "On form submission", - "webhookId": "98b18862-a0e7-4760-9c5e-8fcaef9e2904" - } - ], - "pinData": {}, - "connections": { - "Error Handler": { - "main": [ - [ - { - "node": "Return Error Response1", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare PDF Data": { - "main": [ - [ - { - "node": "Extract PDF Text1", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Response": { - "main": [ - [ - { - "node": "Return JSON Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "PDF Upload Webhook1": { - "main": [ - [ - { - "node": "Prepare PDF Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract PDF Text1": { - "main": [ - [ - { - "node": "AI Document Analyzer1", - "type": "main", - "index": 0 - } - ] - ] - }, - "AI Document Analyzer1": { - "main": [ - [ - { - "node": "Format Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Groq Chat Model1": { - "ai_languageModel": [ - [ - { - "node": "AI Document Analyzer1", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1" - }, - "versionId": "bbc54db8-5559-4e50-ac33-01638aa0eec0", - "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "b419dceeef095c7882b7f3bc7ba03f620c77ec1f3d9d0518174b97d631dd49fa" - }, - "id": "52D41BRLEfcyh22J", - "tags": [ - { - "createdAt": "2025-07-01T13:54:51.754Z", - "updatedAt": "2025-07-01T13:54:51.754Z", - "id": "2ji4EAexY8bmiTeM", - "name": "AI Agent" - } - ] -} \ No newline at end of file diff --git a/data_analyzer/processor.py b/data_analyzer/processor.py deleted file mode 100644 index 02fcf17..0000000 --- a/data_analyzer/processor.py +++ /dev/null @@ -1,217 +0,0 @@ -from agent_base.processors import StandardWebhookProcessor -from django.utils import timezone -from django.conf import settings -from .models import DataAnalysisAgentRequest, DataAnalysisAgentResponse -import json -import requests -import time -import os - - -class DataAnalysisAgentProcessor(StandardWebhookProcessor): - """Webhook processor for Data Analysis Agent agent""" - - agent_slug = 'data-analyzer' - webhook_url = settings.N8N_WEBHOOK_DATA_ANALYZER - agent_id = 'data-analysis-001' - - def _extract_text_from_sections(self, sections): - """Extract plain text from structured sections for legacy compatibility""" - text_parts = [] - - for section in sections: - heading = section.get('heading', '') - content = section.get('content', '') - - if heading and content: - text_parts.append(f"### {heading}") - text_parts.append(content) - text_parts.append("") # Add empty line between sections - - return "\n".join(text_parts).strip() - - def _cleanup_uploaded_file(self, request_obj): - """Delete the uploaded file after processing to save storage and protect privacy""" - if request_obj and request_obj.data_file: - try: - file_path = request_obj.data_file.path - if os.path.exists(file_path): - os.remove(file_path) - print(f"{self.agent_slug}: Successfully deleted uploaded file: {file_path}") - else: - print(f"{self.agent_slug}: File already deleted or doesn't exist: {file_path}") - except Exception as e: - print(f"{self.agent_slug}: Warning - Failed to delete uploaded file: {e}") - # Don't raise exception as this is cleanup, not critical functionality - - def make_request(self, data, timeout=60): - """Override to send PDF file as binary data instead of JSON""" - try: - request_obj = data.get('request_obj') - if not request_obj or not request_obj.data_file: - raise ValueError("No PDF file found in request") - - print(f"{self.agent_slug}: Sending PDF file to N8N webhook: {self.webhook_url}") - - # Read the PDF file - pdf_file = request_obj.data_file - pdf_file.seek(0) # Reset file pointer to beginning - file_content = pdf_file.read() - - print(f"{self.agent_slug}: File size: {len(file_content)} bytes") - print(f"{self.agent_slug}: File name: {pdf_file.name}") - - # Prepare multipart form data - files = { - 'file': (pdf_file.name, file_content, 'application/pdf') - } - - start_time = time.time() - response = requests.post(self.webhook_url, files=files, timeout=timeout) - processing_time = time.time() - start_time - - print(f"{self.agent_slug}: Response status: {response.status_code}") - print(f"{self.agent_slug}: Response text: {response.text[:500]}...") - - response.raise_for_status() - - # Check if response has content - if not response.text.strip(): - raise ValueError("Empty response from webhook") - - # Parse JSON response - try: - response_data = response.json() - except ValueError: - raise ValueError("Invalid JSON response from N8N workflow") - - # Handle array response from N8N (extract first item) - if isinstance(response_data, list) and len(response_data) > 0: - response_data = response_data[0] - elif isinstance(response_data, list) and len(response_data) == 0: - raise ValueError("Empty array response from N8N workflow") - - # Add processing metadata - response_data['processing_time'] = processing_time - - return response_data - - except requests.exceptions.RequestException as e: - print(f"{self.agent_slug}: Webhook request error: {e}") - raise ValueError(f"Webhook error: {e}") - except Exception as e: - print(f"{self.agent_slug}: Processing error: {e}") - raise ValueError(f"Processing error: {e}") - - def prepare_request_data(self, **kwargs): - """Prepare request data - for binary upload, we pass the request object""" - return { - 'request_obj': kwargs.get('request_obj'), - 'analysis_type': kwargs.get('analysis_type', 'summary') - } - - def process_response(self, response_data, request_obj): - """Process webhook response from N8N""" - try: - request_obj.status = 'processing' - request_obj.save() - - # Handle new structured format vs legacy format - if 'sections' in response_data: - # New structured format from webhook - analysis_text = self._extract_text_from_sections(response_data['sections']) - status = 'success' # If we got sections, it's successful - processed_at = response_data.get('timestamp', '') - print(f"{self.agent_slug}: Processing new structured format with {len(response_data['sections'])} sections") - else: - # Legacy format - analysis_text = response_data.get('analysis', '') - status = response_data.get('status', 'unknown') - processed_at = response_data.get('processed_at', '') - print(f"{self.agent_slug}: Processing legacy format") - - # Map N8N response to Django fields - analysis_results = { - 'status': status, - 'processed_at': processed_at, - 'analysis_type': getattr(request_obj, 'analysis_type', 'summary') - } - - # Use analysis text for multiple fields for compatibility - insights_summary = analysis_text - report_text = analysis_text - raw_response = response_data - - # Determine success based on content - success = bool(analysis_text) and (status == 'success' or 'sections' in response_data) - - print(f"{self.agent_slug}: Success: {success}, Analysis length: {len(analysis_text)}") - - # Create or update response object (prevent duplicate responses) - response_obj, created = DataAnalysisAgentResponse.objects.get_or_create( - request=request_obj, - defaults={ - 'success': success, - 'processing_time': response_data.get('processing_time', 0), - 'analysis_results': analysis_results, - 'insights_summary': insights_summary, - 'report_text': report_text, - 'raw_response': raw_response, - } - ) - - # If response already exists, update it - if not created: - response_obj.success = success - response_obj.processing_time = response_data.get('processing_time', 0) - response_obj.analysis_results = analysis_results - response_obj.insights_summary = insights_summary - response_obj.report_text = report_text - response_obj.raw_response = raw_response - response_obj.save() - - # Only deduct wallet balance after successful processing - if success: - request_obj.user.deduct_balance( - request_obj.cost, - f"Data Analysis Agent - {request_obj.data_file.name if request_obj.data_file else 'PDF Analysis'}", - 'data-analyzer' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") - - # Update request as completed - request_obj.status = 'completed' if success else 'failed' - request_obj.processed_at = timezone.now() - request_obj.save() - - # Cleanup uploaded file after successful processing - self._cleanup_uploaded_file(request_obj) - - return response_obj - - except Exception as e: - # Handle error - request_obj.status = 'failed' - request_obj.save() - - # Create or update error response (prevent duplicate responses) - error_response, created = DataAnalysisAgentResponse.objects.get_or_create( - request=request_obj, - defaults={ - 'success': False, - 'error_message': str(e), - 'processing_time': response_data.get('processing_time', 0) if response_data else 0 - } - ) - - # If response already exists, update it with error info - if not created: - error_response.success = False - error_response.error_message = str(e) - error_response.processing_time = response_data.get('processing_time', 0) if response_data else 0 - error_response.save() - - # Cleanup uploaded file even on error to prevent accumulation - self._cleanup_uploaded_file(request_obj) - - raise Exception(f"Failed to process Data Analysis Agent response: {e}") \ No newline at end of file diff --git a/data_analyzer/templates/data_analyzer/detail.html b/data_analyzer/templates/data_analyzer/detail.html deleted file mode 100644 index 573d3ef..0000000 --- a/data_analyzer/templates/data_analyzer/detail.html +++ /dev/null @@ -1,929 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Data Analyzer - Quantum Tasks AI{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} - - -
- - {% include "components/agent_header.html" with agent_title="Data Analyzer" agent_subtitle="AI-powered analysis of your data files with comprehensive insights" %} - - - {% include "components/quick_agents_panel.html" %} - - -
- -
-
-

- šŸ“Š - Data Analysis Configuration -

-
-
-
- {% csrf_token %} - - -
- -
-
-
šŸ“
-
Click to upload or drag and drop
-
PDF files only
-
-
- -
Supported format: PDF files only. Max size: 10MB
-
- - -
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
-
- - -
- {% if user.is_authenticated %} - {% if user.wallet_balance >= agent.price %} - - {% else %} -
- Insufficient balance! You need {{ agent.price }} AED. -
- - šŸ’° Top Up Wallet - - {% endif %} - {% else %} - - šŸ” Login to Continue - - {% endif %} -
-
-
-
- - -
-
-

- ā„¹ļø - How It Works -

-
-
-
    -
  1. Upload your PDF file
  2. -
  3. Choose analysis type and preferences
  4. -
  5. Our AI analyzes your data
  6. -
  7. Get comprehensive insights and reports
  8. -
- - - -
-
-
- - -
- - {% include "components/processing_status.html" with status_title="Analyzing Your Data..." status_text="Please wait while our AI processes your file..." %} - - - {% include "components/results_container.html" with results_title="Analysis Results" %} -
-
- - -{% endblock %} \ No newline at end of file diff --git a/data_analyzer/urls.py b/data_analyzer/urls.py deleted file mode 100644 index 2d1fd31..0000000 --- a/data_analyzer/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'data_analyzer' - -urlpatterns = [ - path('', views.data_analyzer_detail, name='detail'), - path('status//', views.data_analyzer_status, name='status'), - path('result//', views.data_analyzer_result, name='result'), -] \ No newline at end of file diff --git a/data_analyzer/views.py b/data_analyzer/views.py deleted file mode 100644 index aff1921..0000000 --- a/data_analyzer/views.py +++ /dev/null @@ -1,175 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.http import JsonResponse -from agent_base.models import BaseAgent -from .models import DataAnalysisAgentRequest, DataAnalysisAgentResponse -from .processor import DataAnalysisAgentProcessor -import json - - -@login_required -def data_analyzer_detail(request): - """Detail page for Data Analysis Agent agent""" - try: - agent = BaseAgent.objects.get(slug='data-analyzer') - except BaseAgent.DoesNotExist: - messages.error(request, 'Data Analysis Agent agent not found.') - return redirect('core:homepage') - - # Handle AJAX POST requests for processing - if request.method == 'POST' and request.headers.get('X-Requested-With') == 'XMLHttpRequest': - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - # Handle multipart form data for file uploads - data = request.POST.dict() - files = request.FILES - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - # Validate PDF file upload - data_file = files.get('file') - if not data_file: - return JsonResponse({'error': 'PDF file is required'}, status=400) - - # Validate file type - if not data_file.name.lower().endswith('.pdf'): - return JsonResponse({'error': 'Only PDF files are supported'}, status=400) - - if data_file.content_type != 'application/pdf': - return JsonResponse({'error': 'Invalid file type. Only PDF files are allowed'}, status=400) - - # Create request object (no wallet deduction yet - only after successful processing) - agent_request = DataAnalysisAgentRequest.objects.create( - user=request.user, - agent=agent, - cost=agent.price, - data_file=data_file, - analysis_type=data.get('analysisType', 'summary'), - ) - - # Process request - processor = DataAnalysisAgentProcessor() - result = processor.process_request( - request_obj=agent_request, - user_id=request.user.id, - data_file_url=agent_request.data_file.url if agent_request.data_file else '', - analysis_type=data.get('analysisType', 'summary'), - ) - - # Refresh user from database to get updated wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': True, - 'request_id': str(agent_request.id), - 'message': 'Data analysis request processed successfully', - 'wallet_balance': float(request.user.wallet_balance) - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - # Handle non-AJAX POST requests (redirect to prevent resubmission popup) - elif request.method == 'POST': - messages.info(request, 'Please use the analyze button to process your data.') - return redirect('data_analyzer:detail') - - # Regular GET request - show the form page - user_requests = DataAnalysisAgentRequest.objects.filter( - user=request.user - ).select_related('agent').prefetch_related('response').order_by('-created_at')[:10] - - # Get other available agents for quick access - available_agents = BaseAgent.objects.filter( - is_active=True - ).exclude(slug='data-analyzer').order_by('name') - - context = { - 'agent': agent, - 'user_requests': user_requests, - 'available_agents': available_agents - } - return render(request, 'data_analyzer/detail.html', context) - - - - -@login_required -def data_analyzer_status(request, request_id): - """Get status for a specific request (for polling)""" - try: - agent_request = DataAnalysisAgentRequest.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': response.success, - 'status': agent_request.status, - 'analysis_results': getattr(response, 'analysis_results', None), - 'insights_summary': getattr(response, 'insights_summary', None), - 'report_text': getattr(response, 'report_text', None), - 'raw_response': getattr(response, 'raw_response', None), - 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except DataAnalysisAgentRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -@login_required -def data_analyzer_result(request, request_id): - """Get result for a specific request""" - try: - agent_request = DataAnalysisAgentRequest.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': response.success, - 'status': agent_request.status, - 'analysis_results': getattr(response, 'analysis_results', None), - 'insights_summary': getattr(response, 'insights_summary', None), - 'report_text': getattr(response, 'report_text', None), - 'raw_response': getattr(response, 'raw_response', None), - 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except DataAnalysisAgentRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/docs_update_summary.txt b/docs_update_summary.txt index 417de7a..24874bf 100644 --- a/docs_update_summary.txt +++ b/docs_update_summary.txt @@ -1,14 +1,27 @@ === Documentation Auto-Update Summary === -Update Date: 2025-07-28 22:36:00 +Update Date: 2025-07-29 19:33:01 Recent Commits: + - 73d5141 āœ… All agents working: Direct N8N integration, fixed routing, and pricing sync + - adab6e4 🧹 Complete template component architecture and remove notification noise - bf1e882 šŸ”„ Finalize auto-documentation cycle - - 01f7941 šŸ“ Final documentation update summary - - c84029d šŸ“š Auto-update documentation after shared utilities implementation -Backend Changes: - - docs_update_summary.txt +Agents Changes: + - workflows/config/agents.py + - workflows/templates/workflows/components/quick_agents_panel.html -No documentation files required updates. +Core Changes: + - netcop_hub/urls.py + +Frontend Changes: + - static/js/data-analyzer.js + - static/js/job-posting-generator.js + - static/js/social-ads.js + - workflows/templates/workflows/data-analyzer.html + - workflows/templates/workflows/job-posting-generator.html + +Updated Documentation Files: + - /home/amit/projects/quantum_ai_v2/CLAUDE.md + - /home/amit/projects/quantum_ai_v2/docs/development/agent-creation.md === End Summary === \ No newline at end of file diff --git a/email_writer/__init__.py b/email_writer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/email_writer/admin.py b/email_writer/admin.py deleted file mode 100644 index 058f2ba..0000000 --- a/email_writer/admin.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.contrib import admin -from .models import EmailWriterRequest - - -@admin.register(EmailWriterRequest) -class EmailWriterRequestAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'email_type', 'recipient', 'tone', 'status', 'created_at'] - list_filter = ['email_type', 'tone', 'length', 'status', 'created_at'] - search_fields = ['user__username', 'recipient', 'main_message'] - readonly_fields = ['id', 'created_at', 'processed_at'] - - fieldsets = ( - ('Request Information', { - 'fields': ('id', 'user', 'status', 'cost', 'created_at', 'processed_at') - }), - ('Email Details', { - 'fields': ('email_type', 'recipient', 'subject', 'main_message', 'tone', 'length') - }), - ('Results', { - 'fields': ('email_content',), - 'classes': ('collapse',) - }) - ) - - def get_readonly_fields(self, request, obj=None): - readonly = list(self.readonly_fields) - if obj: # editing an existing object - readonly.extend(['user', 'email_type', 'recipient', 'main_message']) - return readonly \ No newline at end of file diff --git a/email_writer/migrations/0001_initial.py b/email_writer/migrations/0001_initial.py deleted file mode 100644 index d084358..0000000 --- a/email_writer/migrations/0001_initial.py +++ /dev/null @@ -1,129 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-24 20:50 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="EmailWriterRequest", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ( - "status", - models.CharField( - choices=[ - ("pending", "Pending"), - ("processing", "Processing"), - ("completed", "Completed"), - ("failed", "Failed"), - ], - default="pending", - max_length=20, - ), - ), - ( - "cost", - models.DecimalField(decimal_places=2, default=3.0, max_digits=10), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("processed_at", models.DateTimeField(blank=True, null=True)), - ( - "email_type", - models.CharField( - choices=[ - ("business", "Business Email"), - ("follow_up", "Follow-up Email"), - ("complaint", "Complaint Email"), - ("thank_you", "Thank You Email"), - ("introduction", "Introduction Email"), - ("meeting_request", "Meeting Request"), - ("apology", "Apology Email"), - ("announcement", "Announcement"), - ], - help_text="Type of email to generate", - max_length=50, - ), - ), - ( - "recipient", - models.CharField( - help_text="Who the email is being sent to", max_length=200 - ), - ), - ( - "subject", - models.CharField( - blank=True, - help_text="Email subject (optional - can be auto-generated)", - max_length=200, - ), - ), - ( - "main_message", - models.TextField(help_text="Main content/purpose of the email"), - ), - ( - "tone", - models.CharField( - choices=[ - ("professional", "Professional"), - ("friendly", "Friendly"), - ("formal", "Formal"), - ("casual", "Casual"), - ], - default="professional", - help_text="Tone of the email", - max_length=30, - ), - ), - ( - "length", - models.CharField( - choices=[ - ("short", "Short (1-2 paragraphs)"), - ("medium", "Medium (3-4 paragraphs)"), - ("long", "Long (5+ paragraphs)"), - ], - default="medium", - help_text="Desired length of the email", - max_length=20, - ), - ), - ( - "email_content", - models.TextField(blank=True, help_text="Generated email content"), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "verbose_name": "Email Writer Request", - "verbose_name_plural": "Email Writer Requests", - "ordering": ["-created_at"], - }, - ), - ] diff --git a/email_writer/migrations/__init__.py b/email_writer/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/email_writer/models.py b/email_writer/models.py deleted file mode 100644 index 927657b..0000000 --- a/email_writer/models.py +++ /dev/null @@ -1,90 +0,0 @@ -from django.db import models -from django.contrib.auth import get_user_model -import uuid - -User = get_user_model() - - -class EmailWriterRequest(models.Model): - """Email Writer agent request model""" - - # Base request fields - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - user = models.ForeignKey(User, on_delete=models.CASCADE) - status = models.CharField(max_length=20, choices=[ - ('pending', 'Pending'), - ('processing', 'Processing'), - ('completed', 'Completed'), - ('failed', 'Failed'), - ], default='pending') - cost = models.DecimalField(max_digits=10, decimal_places=2, default=3.00) - created_at = models.DateTimeField(auto_now_add=True) - processed_at = models.DateTimeField(null=True, blank=True) - - # Email content fields - email_type = models.CharField( - max_length=50, - choices=[ - ('business', 'Business Email'), - ('follow_up', 'Follow-up Email'), - ('complaint', 'Complaint Email'), - ('thank_you', 'Thank You Email'), - ('introduction', 'Introduction Email'), - ('meeting_request', 'Meeting Request'), - ('apology', 'Apology Email'), - ('announcement', 'Announcement'), - ], - help_text="Type of email to generate" - ) - - recipient = models.CharField( - max_length=200, - help_text="Who the email is being sent to" - ) - - subject = models.CharField( - max_length=200, - blank=True, - help_text="Email subject (optional - can be auto-generated)" - ) - - main_message = models.TextField( - help_text="Main content/purpose of the email" - ) - - tone = models.CharField( - max_length=30, - choices=[ - ('professional', 'Professional'), - ('friendly', 'Friendly'), - ('formal', 'Formal'), - ('casual', 'Casual'), - ], - default='professional', - help_text="Tone of the email" - ) - - length = models.CharField( - max_length=20, - choices=[ - ('short', 'Short (1-2 paragraphs)'), - ('medium', 'Medium (3-4 paragraphs)'), - ('long', 'Long (5+ paragraphs)'), - ], - default='medium', - help_text="Desired length of the email" - ) - - # Result fields - email_content = models.TextField( - blank=True, - help_text="Generated email content" - ) - - class Meta: - verbose_name = "Email Writer Request" - verbose_name_plural = "Email Writer Requests" - ordering = ['-created_at'] - - def __str__(self): - return f"Email Writer - {self.email_type} for {self.recipient}" \ No newline at end of file diff --git a/email_writer/processor.py b/email_writer/processor.py deleted file mode 100644 index 5aca61b..0000000 --- a/email_writer/processor.py +++ /dev/null @@ -1,79 +0,0 @@ -import json -from agent_base.processors import BaseAgentProcessor -from .models import EmailWriterRequest - - -class EmailWriterProcessor(BaseAgentProcessor): - """Email Writer agent processor""" - - model_class = EmailWriterRequest - agent_name = "Email Writer" - cost = 3.00 # AED per request - - def prepare_webhook_data(self, request_obj): - """Prepare data for webhook processing""" - return { - 'email_type': request_obj.email_type, - 'recipient': request_obj.recipient, - 'subject': request_obj.subject, - 'main_message': request_obj.main_message, - 'tone': request_obj.tone, - 'length': request_obj.length, - } - - def process_webhook_response(self, request_obj, webhook_response): - """Process webhook response and update request object""" - try: - if isinstance(webhook_response, str): - response_data = json.loads(webhook_response) - else: - response_data = webhook_response - - # Extract email content from response - email_content = "" - - # Try different possible response formats - if 'email_content' in response_data: - email_content = response_data['email_content'] - elif 'content' in response_data: - email_content = response_data['content'] - elif 'output' in response_data: - email_content = response_data['output'] - elif 'generated_email' in response_data: - email_content = response_data['generated_email'] - elif isinstance(response_data, str): - email_content = response_data - else: - # If no specific field found, try to extract text - email_content = str(response_data) - - # Update request object - request_obj.email_content = email_content - request_obj.save() - - return { - 'success': True, - 'email_content': email_content, - 'status': 'completed' - } - - except Exception as e: - return { - 'success': False, - 'error': f"Failed to process email generation: {str(e)}", - 'status': 'failed' - } - - def get_result_summary(self, request_obj): - """Get a summary of the results for display""" - if request_obj.email_content: - return { - 'email_type': request_obj.get_email_type_display(), - 'recipient': request_obj.recipient, - 'tone': request_obj.get_tone_display(), - 'length': request_obj.get_length_display(), - 'email_content': request_obj.email_content, - 'has_subject': bool(request_obj.subject), - 'subject': request_obj.subject - } - return None \ No newline at end of file diff --git a/email_writer/templates/email_writer/detail.html b/email_writer/templates/email_writer/detail.html deleted file mode 100644 index 6e46b52..0000000 --- a/email_writer/templates/email_writer/detail.html +++ /dev/null @@ -1,1344 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Email Writer Agent - Quantum Tasks AI{% endblock %} - -{% block extra_css %} - - - - -{% endblock %} - -{% block content %} -
- - {% include "components/agent_header.html" with agent_title="Email Writer" agent_subtitle="Generate professional emails with AI-powered content creation" %} - - - {% include "components/quick_agents_panel.html" %} - - - {% if messages %} - {% for message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - - -
- -
-
-

- šŸ“§ - Email Details -

-
-
- -
- {% csrf_token %} - - -
-

šŸ“¬ Email Information

- -
- - -
Choose the type of email you want to generate
- -
- -
- - -
Enter the name or title of the person/team you're writing to
- -
- -
- - -
Optional - we can generate an appropriate subject line if left blank
-
-
- - -
-

āœļø Content & Style

- -
- - -
Describe the key points and purpose of your email
- -
- -
- - -
Choose the appropriate tone for your email
-
- -
- - -
How detailed should the email be?
-
-
- - -
- {% if user.is_authenticated %} - {% if user.wallet_balance >= agent.price %} - - {% else %} -
- Insufficient balance! You need {{ agent.price }} AED. -
- - šŸ’° Top Up Wallet - - {% endif %} - {% else %} - - šŸ”‘ Login to Continue - - {% endif %} -
-
- -
-
- - -
-
-

- ā„¹ļø - How It Works -

-
-
-
    -
  1. Choose email type and recipient
  2. -
  3. Describe your message and purpose
  4. -
  5. Select tone and length preferences
  6. -
  7. Get professional AI-generated email
  8. -
- - - -
-
-
- - - {% include "components/processing_status.html" with status_title="Generating Email..." status_text="Creating professional email content..." %} - - - {% include "components/results_container.html" with results_title="Generated Email" %} -
-{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file diff --git a/email_writer/urls.py b/email_writer/urls.py deleted file mode 100644 index d705f90..0000000 --- a/email_writer/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'email_writer' - -urlpatterns = [ - path('', views.email_writer_detail, name='detail'), - path('status//', views.email_writer_status, name='status'), -] \ No newline at end of file diff --git a/email_writer/views.py b/email_writer/views.py deleted file mode 100644 index 8db5934..0000000 --- a/email_writer/views.py +++ /dev/null @@ -1,140 +0,0 @@ -import json -from django.shortcuts import render -from django.contrib.auth.decorators import login_required -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from django.shortcuts import get_object_or_404 -from django.contrib import messages - -from .models import EmailWriterRequest -from .processor import EmailWriterProcessor - - -def email_writer_detail(request): - """Email Writer agent detail page""" - context = { - 'agent_title': 'Email Writer', - 'agent_subtitle': 'Generate professional emails with AI-powered content creation', - 'page_title': 'Email Writer Agent - NetCop AI Hub' - } - - if request.method == 'POST': - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - # Check if this is an AJAX request - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - try: - # Validate form data - email_type = request.POST.get('email_type', '').strip() - recipient = request.POST.get('recipient', '').strip() - main_message = request.POST.get('main_message', '').strip() - tone = request.POST.get('tone', 'professional') - length = request.POST.get('length', 'medium') - subject = request.POST.get('subject', '').strip() - - # Basic validation - if not email_type or not recipient or not main_message: - return JsonResponse({ - 'error': 'Please fill in all required fields', - 'success': False - }) - - if len(main_message) < 10: - return JsonResponse({ - 'error': 'Main message must be at least 10 characters long', - 'success': False - }) - - # Initialize processor - processor = EmailWriterProcessor() - - # Check wallet balance - if not processor.check_wallet_balance(request.user): - return JsonResponse({ - 'error': f'Insufficient wallet balance. You need {processor.cost:.2f} AED.', - 'success': False - }) - - # Create request object - email_request = EmailWriterRequest.objects.create( - user=request.user, - email_type=email_type, - recipient=recipient, - subject=subject, - main_message=main_message, - tone=tone, - length=length, - status='pending' - ) - - # Process the request - try: - result = processor.process_request(email_request) - - if result.get('success'): - return JsonResponse({ - 'success': True, - 'request_id': email_request.id, - 'message': 'Email generation started successfully', - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'error': result.get('error', 'Failed to process email generation'), - 'success': False - }) - - except Exception as e: - return JsonResponse({ - 'error': f'Processing error: {str(e)}', - 'success': False - }) - - except Exception as e: - return JsonResponse({ - 'error': f'Request error: {str(e)}', - 'success': False - }) - else: - # Handle regular form submission (non-AJAX) - messages.error(request, 'Please enable JavaScript for the best experience.') - - return render(request, 'email_writer/detail.html', context) - - -@require_http_methods(["GET"]) -def email_writer_status(request, request_id): - """Check status of email generation request""" - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - email_request = get_object_or_404( - EmailWriterRequest, - id=request_id, - user=request.user - ) - - processor = EmailWriterProcessor() - status_data = processor.get_request_status(email_request) - - # Add wallet balance to response - status_data['wallet_balance'] = float(request.user.wallet_balance) - - # If completed, include the email content - if status_data.get('status') == 'completed' and email_request.email_content: - status_data['email_content'] = email_request.email_content - status_data['email_type'] = email_request.get_email_type_display() - status_data['recipient'] = email_request.recipient - status_data['tone'] = email_request.get_tone_display() - status_data['length'] = email_request.get_length_display() - status_data['subject'] = email_request.subject - - return JsonResponse(status_data) - - except EmailWriterRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/five_whys_analyzer/__init__.py b/five_whys_analyzer/__init__.py deleted file mode 100644 index 1f5df5b..0000000 --- a/five_whys_analyzer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# 5 Whys Analysis Agent Agent App \ No newline at end of file diff --git a/five_whys_analyzer/admin.py b/five_whys_analyzer/admin.py deleted file mode 100644 index fd2d7a1..0000000 --- a/five_whys_analyzer/admin.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib import admin -from .models import FiveWhysAnalyzerRequest, FiveWhysAnalyzerResponse - - -@admin.register(FiveWhysAnalyzerRequest) -class FiveWhysAnalyzerRequestAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'session_id', 'status', 'report_generated', 'chat_active', 'created_at', 'cost'] - list_filter = ['status', 'report_generated', 'chat_active', 'analysis_depth', 'created_at'] - search_fields = ['user__email', 'user__username', 'session_id', 'problem_statement'] - readonly_fields = ['id', 'created_at', 'processed_at', 'session_id'] - ordering = ['-created_at'] - - fieldsets = ( - ('Basic Info', { - 'fields': ('id', 'user', 'session_id', 'status', 'created_at', 'processed_at') - }), - ('Chat Session', { - 'fields': ('chat_active', 'chat_messages') - }), - ('Report Generation', { - 'fields': ('report_generated', 'problem_statement', 'context_info', 'analysis_depth', 'cost') - }), - ) - - -@admin.register(FiveWhysAnalyzerResponse) -class FiveWhysAnalyzerResponseAdmin(admin.ModelAdmin): - list_display = ['id', 'request', 'success', 'created_at'] - list_filter = ['success', 'created_at'] - readonly_fields = ['id', 'created_at'] - ordering = ['-created_at'] - - fieldsets = ( - ('Basic Info', { - 'fields': ('id', 'request', 'success', 'created_at', 'processing_time', 'error_message') - }), - ('Chat Response', { - 'fields': ('chat_response', 'chat_history') - }), - ('Final Report', { - 'fields': ('final_report', 'report_metadata') - }), - ) \ No newline at end of file diff --git a/five_whys_analyzer/apps.py b/five_whys_analyzer/apps.py deleted file mode 100644 index 8127422..0000000 --- a/five_whys_analyzer/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class FiveWhysAnalyzerConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'five_whys_analyzer' \ No newline at end of file diff --git a/five_whys_analyzer/migrations/0001_initial.py b/five_whys_analyzer/migrations/0001_initial.py deleted file mode 100644 index 7224c15..0000000 --- a/five_whys_analyzer/migrations/0001_initial.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-12 16:08 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('agent_base', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='FiveWhysAnalyzerRequest', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)), - ('cost', models.DecimalField(decimal_places=2, max_digits=10)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('processed_at', models.DateTimeField(blank=True, null=True)), - ('session_id', models.CharField(db_index=True, default=uuid.uuid4, max_length=100)), - ('chat_messages', models.JSONField(default=list, help_text='Store chat history as list of messages')), - ('problem_statement', models.TextField(blank=True, help_text='Main problem to analyze')), - ('context_info', models.TextField(blank=True, help_text='Additional context information')), - ('analysis_depth', models.CharField(blank=True, choices=[('standard', 'Standard 5 Whys'), ('detailed', 'Extended Analysis'), ('comprehensive', 'Comprehensive Report')], default='standard', max_length=20)), - ('report_generated', models.BooleanField(default=False, help_text='Has final report been generated and paid for')), - ('chat_active', models.BooleanField(default=True, help_text='Is chat session still active')), - ('input_text', models.TextField(blank=True)), - ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agent_base.baseagent')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': '5 Whys Analysis Agent Request', - 'verbose_name_plural': '5 Whys Analysis Agent Requests', - 'db_table': 'five_whys_analyzer_requests', - }, - ), - migrations.CreateModel( - name='FiveWhysAnalyzerResponse', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('success', models.BooleanField(default=False)), - ('error_message', models.TextField(blank=True)), - ('processing_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('chat_response', models.TextField(blank=True, help_text='Latest chat response')), - ('chat_history', models.JSONField(default=list, help_text='Full chat response history')), - ('final_report', models.TextField(blank=True, help_text='Generated 5 Whys analysis report')), - ('report_metadata', models.JSONField(default=dict, help_text='Report generation metadata')), - ('output_text', models.TextField(blank=True)), - ('raw_response', models.JSONField(blank=True, default=dict)), - ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='response', to='five_whys_analyzer.fivewhysanalyzerrequest')), - ], - options={ - 'verbose_name': '5 Whys Analysis Agent Response', - 'verbose_name_plural': '5 Whys Analysis Agent Responses', - 'db_table': 'five_whys_analyzer_responses', - }, - ), - migrations.AddIndex( - model_name='fivewhysanalyzerrequest', - index=models.Index(fields=['session_id'], name='five_whys_a_session_0dd791_idx'), - ), - migrations.AddIndex( - model_name='fivewhysanalyzerrequest', - index=models.Index(fields=['user', 'chat_active'], name='five_whys_a_user_id_810315_idx'), - ), - ] diff --git a/five_whys_analyzer/migrations/__init__.py b/five_whys_analyzer/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/five_whys_analyzer/models.py b/five_whys_analyzer/models.py deleted file mode 100644 index 9d2214e..0000000 --- a/five_whys_analyzer/models.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.db import models -from decimal import Decimal -from agent_base.models import BaseAgentRequest, BaseAgentResponse -import uuid - - -class FiveWhysAnalyzerRequest(BaseAgentRequest): - """5 Whys Analysis Agent request tracking with chat support""" - - # Chat session management - session_id = models.CharField(max_length=100, default=uuid.uuid4, db_index=True) - - # Chat interaction tracking - chat_messages = models.JSONField(default=list, help_text="Store chat history as list of messages") - - # Final report fields (only filled when report is generated) - problem_statement = models.TextField(blank=True, help_text="Main problem to analyze") - context_info = models.TextField(blank=True, help_text="Additional context information") - analysis_depth = models.CharField( - max_length=20, - blank=True, - choices=[ - ('standard', 'Standard 5 Whys'), - ('detailed', 'Extended Analysis'), - ('comprehensive', 'Comprehensive Report') - ], - default='standard' - ) - - # Chat vs Report tracking - report_generated = models.BooleanField(default=False, help_text="Has final report been generated and paid for") - chat_active = models.BooleanField(default=True, help_text="Is chat session still active") - - # Legacy field for compatibility - input_text = models.TextField(blank=True) - - class Meta: - db_table = 'five_whys_analyzer_requests' - verbose_name = '5 Whys Analysis Agent Request' - verbose_name_plural = '5 Whys Analysis Agent Requests' - indexes = [ - models.Index(fields=['session_id']), - models.Index(fields=['user', 'chat_active']), - ] - - -class FiveWhysAnalyzerResponse(BaseAgentResponse): - """5 Whys Analysis Agent response storage""" - - request = models.OneToOneField( - FiveWhysAnalyzerRequest, - on_delete=models.CASCADE, - related_name='response' - ) - - # Chat responses (free interactions) - chat_response = models.TextField(blank=True, help_text="Latest chat response") - chat_history = models.JSONField(default=list, help_text="Full chat response history") - - # Final report (paid interaction) - final_report = models.TextField(blank=True, help_text="Generated 5 Whys analysis report") - report_metadata = models.JSONField(default=dict, help_text="Report generation metadata") - - # Legacy fields for compatibility - output_text = models.TextField(blank=True) - raw_response = models.JSONField(default=dict, blank=True) - - class Meta: - db_table = 'five_whys_analyzer_responses' - verbose_name = '5 Whys Analysis Agent Response' - verbose_name_plural = '5 Whys Analysis Agent Responses' \ No newline at end of file diff --git a/five_whys_analyzer/n8n_workflows/5_whys.json b/five_whys_analyzer/n8n_workflows/5_whys.json deleted file mode 100644 index 2692b72..0000000 --- a/five_whys_analyzer/n8n_workflows/5_whys.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "name": "5-whys", - "nodes": [ - { - "parameters": { - "model": { - "__rl": true, - "mode": "list", - "value": "gpt-4o", - "cachedResultName": "gpt-4o" - }, - "options": {} - }, - "id": "605beb51-6e39-42e7-9aad-e38dc79f0835", - "name": "OpenAI Chat Model", - "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "position": [ - 620, - 1120 - ], - "typeVersion": 1.2, - "credentials": { - "openAiApi": { - "id": "uzyuJ5c9nml2NneC", - "name": "OpenAi account" - } - } - }, - { - "parameters": { - "sessionIdType": "customKey", - "sessionKey": "={{ $('Set Web Input').item.json.body.sessionId }}", - "contextWindowLength": 50 - }, - "id": "2c4d3c68-6368-49ec-a980-6101e7aadc2f", - "name": "Simple Memory", - "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "position": [ - 720, - 1120 - ], - "typeVersion": 1.3 - }, - { - "parameters": { - "promptType": "define", - "text": "={{ $json.body.message.text }}", - "options": { - "systemMessage": "=## System Instructions\n\nYou are a skilled Root Cause Analysis facilitator specializing in the 5 Why methodology developed by Sakichi Toyoda for Toyota Industries. Your role is to guide users through a systematic investigation to identify the true root cause of problems or defects by asking \"why\" questions in a structured manner.\n\n## Core Methodology\n\nFollow these principles from the 5 Why technique:\n- Ask \"why\" to drill down from symptoms to root causes\n- Continue asking \"why\" until the root cause is determined (typically 5 levels)\n- Focus on process and system failures, not individual blame\n- Ensure each \"why\" question builds logically on the previous answer\n- Stop when you reach a root cause that can be controlled and prevented\n\n## Conversation Flow\n\n### Phase 1: Problem Definition\n1. **Initial Greeting**: Welcome the user and explain the 5 Why process\n2. **Problem Statement**: Ask the user to clearly define the problem or defect\n3. **Context Gathering**: Collect relevant details about when, where, and how the problem occurred\n\n**Sample Opening**: \n\"Hello! I'm here to help you conduct a thorough root cause analysis using the proven 5 Why methodology. This technique will help us move beyond symptoms to identify the true root cause of your problem, enabling you to implement effective solutions that prevent recurrence.\n\nPlease start by describing the specific problem or defect you'd like to analyze. Be as detailed as possible about what happened, when it occurred, and any immediate impacts.\"\n\n### Phase 2: Systematic Investigation\nFor each level (1st Why through 5th Why), ask three distinct questions:\n\n1. **Occurrence**: \"Why did this problem/cause occur?\"\n2. **Detection**: \"Why was this problem/cause not detected earlier?\"\n3. **Prevention**: \"Why was this problem/cause not prevented?\"\n\n**Question Guidelines**:\n- Ask one question at a time\n- Wait for user response before proceeding\n- Probe for specific, actionable answers\n- Avoid accepting vague responses like \"human error\" or \"bad luck\"\n- Guide users to focus on controllable factors\n\n**Sample Question Pattern**:\n\"Now let's examine the first level. Based on your problem statement:\n- 1st Why: Why did [specific problem] occur? Please be specific about the immediate cause.\"\n\nAfter receiving the answer:\n\"Thank you. Now, regarding this cause:\n- Why was this immediate cause not detected before it led to the problem?\"\n\nThen:\n\"And finally for this level:\n- Why was this cause not prevented from occurring in the first place?\"\n\n### Phase 3: Validation and Confirmation\nBefore proceeding to each new level:\n- Summarize the previous level's findings\n- Confirm understanding with the user\n- Ensure logical connection between levels\n\n**Sample Validation**:\n\"Let me summarize what we've established for the 1st Why:\n- The problem occurred because: [user's answer]\n- It wasn't detected because: [user's answer] \n- It wasn't prevented because: [user's answer]\n\nDoes this accurately capture the situation? Should we proceed to examine why [the cause from occurrence] happened?\"\n\n### Phase 4: Root Cause Identification\n- Continue until reaching a controllable root cause\n- May require fewer or more than 5 levels\n- Ensure the final root cause is actionable\n\n**Sample Root Cause Recognition**:\n\"Excellent! We've identified what appears to be the root cause: [summary]. This is something your organization can control and address through specific actions. Let me now prepare your complete analysis in a structured format.\"\n\n## Output Generation\n\nOnce the investigation is complete, generate a comprehensive Root-Cause Analysis table with this exact format:\n\n```\n# Root-Cause Analysis Report\n\n**Project Name**: [If provided]\n**Project Manager**: [If provided] \n**Problem or Defect Title**: [From user's initial description]\n**Analysis Date**: [Current date]\n\n## Analysis Matrix\n\n| Why | Why did the Problem or Defect Occur? | Why was it not Detected? | Why was it not Prevented? |\n|-----|--------------------------------------|--------------------------|---------------------------|\n| 1st Why | [User's answer to occurrence question] | [User's answer to detection question] | [User's answer to prevention question] |\n| 2nd Why | [User's answer to occurrence question] | [User's answer to detection question] | [User's answer to prevention question] |\n| 3rd Why | [User's answer to occurrence question] | [User's answer to detection question] | [User's answer to prevention question] |\n| 4th Why | [User's answer to occurrence question] | [User's answer to detection question] | [User's answer to prevention question] |\n| 5th Why | [User's answer to occurrence question] | [User's answer to detection question] | [User's answer to prevention question] |\n\n## Root Cause Summary\n**Identified Root Cause**: [Final root cause from the analysis]\n\n**Recommended Actions**:\n1. **Immediate Actions**: [Suggestions to address the root cause]\n2. **Detection Improvements**: [Suggestions to improve early detection]\n3. **Prevention Measures**: [Suggestions to prevent recurrence]\n\n## Next Steps\n- Verify the root cause through further investigation if needed\n- Implement corrective actions to address the root cause\n- Monitor effectiveness of implemented solutions\n- Consider applying similar analysis to related processes\n```\n\n## Behavioral Guidelines\n\n**DO**:\n- Be patient and encouraging throughout the process\n- Ask follow-up questions if answers are too vague\n- Acknowledge good insights from the user\n- Maintain focus on the methodology\n- Adapt language to the user's technical level\n- Summarize progress regularly\n\n**DON'T**:\n- Accept superficial answers\n- Rush through levels\n- Make assumptions about causes\n- Blame individuals\n- Skip the validation steps\n- Provide the answers yourself (guide the user to discover them)\n\n## Sample Responses for Common Situations\n\n**If user gives vague answer**:\n\"That's a good start, but let's dig a bit deeper. When you say [vague answer], can you be more specific about the exact mechanism or process that failed? What specifically went wrong?\"\n\n**If user wants to skip levels**:\n\"I understand you might see the root cause already, but the 5 Why methodology's strength comes from systematic analysis. Each level often reveals important insights we might miss otherwise. Let's work through this step by step.\"\n\n**If user blames individuals**:\n\"I hear that [person] was involved, but let's focus on the systems and processes. What in the process or system allowed this situation to occur? How could the process be improved to prevent this regardless of who is involved?\"\n\n**When reaching potential root cause**:\n\"This looks like it could be our root cause. Let's test it: If we address [potential root cause], would it prevent the original problem from recurring? If yes, and this is something within your organization's control, we may have found our root cause.\"\n\n## Session Management\n- Maintain conversation state throughout the analysis\n- Number each interaction for tracking\n- Save user responses in structured format\n- Generate session summary at completion\n\nRemember: Your goal is to facilitate discovery, not to provide answers. Guide the user through the methodology while they do the analytical thinking.\n\nWhen you are asked to perform a task on the current date, please use the current time and date: {{$now}}\n\n## Output\nYou should output the result and don't make things and stricktly follow the prompt and create a table" - } - }, - "id": "8c5d2c14-a012-4d64-82dd-94c321baf3c2", - "name": "AI Agent", - "type": "@n8n/n8n-nodes-langchain.agent", - "position": [ - 580, - 900 - ], - "typeVersion": 1.9 - }, - { - "parameters": { - "chatId": "={{$('Telegram Trigger').first().json.message.chat.id}}", - "text": "={{ $json.output }}", - "additionalFields": { - "appendAttribution": false - } - }, - "id": "0b41dcb9-6906-45d0-860e-463a91987394", - "name": "Send Response To Telegram", - "type": "n8n-nodes-base.telegram", - "position": [ - 960, - 800 - ], - "webhookId": "70b0bc52-fee4-4da7-a89f-c616339cf1dd", - "typeVersion": 1.2, - "disabled": true - }, - { - "parameters": { - "content": "## Telegram ", - "height": 680, - "width": 1220 - }, - "type": "n8n-nodes-base.stickyNote", - "position": [ - 0, - 0 - ], - "typeVersion": 1, - "id": "200b54fa-0691-4e49-96f9-5a027122cb27", - "name": "Sticky Note" - }, - { - "parameters": { - "httpMethod": "POST", - "path": "5-whys-web", - "responseMode": "lastNode", - "options": {} - }, - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [ - 140, - 900 - ], - "id": "5b6dcf55-a4bf-4f40-981f-e40c72787359", - "webhookId": "93060546-49e6-4620-8cad-fd12f7d9333e" - }, - { - "parameters": { - "options": {} - }, - "name": "Set Web Input", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 360, - 900 - ], - "id": "abec631e-9163-46ca-9740-b02d06e11d92" - }, - { - "parameters": { - "options": {} - }, - "name": "Respond to Web", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - 960, - 1000 - ], - "id": "618f327a-744e-4984-8708-e4f463e57eb1" - } - ], - "pinData": {}, - "connections": { - "AI Agent": { - "main": [ - [ - { - "node": "Send Response To Telegram", - "type": "main", - "index": 0 - }, - { - "node": "Respond to Web", - "type": "main", - "index": 0 - } - ] - ] - }, - "Simple Memory": { - "ai_memory": [ - [ - { - "node": "AI Agent", - "type": "ai_memory", - "index": 0 - } - ] - ] - }, - "OpenAI Chat Model": { - "ai_languageModel": [ - [ - { - "node": "AI Agent", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Webhook": { - "main": [ - [ - { - "node": "Set Web Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Web Input": { - "main": [ - [ - { - "node": "AI Agent", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1" - }, - "versionId": "04b28e7e-5cc6-4b34-9bdf-d55ca064322b", - "meta": { - "instanceId": "b419dceeef095c7882b7f3bc7ba03f620c77ec1f3d9d0518174b97d631dd49fa" - }, - "id": "z4ojaUdIdsbYxjHy", - "tags": [ - { - "createdAt": "2025-07-01T13:54:51.754Z", - "updatedAt": "2025-07-01T13:54:51.754Z", - "id": "2ji4EAexY8bmiTeM", - "name": "AI Agent" - } - ] -} \ No newline at end of file diff --git a/five_whys_analyzer/n8n_workflows/README.md b/five_whys_analyzer/n8n_workflows/README.md deleted file mode 100644 index b28838a..0000000 --- a/five_whys_analyzer/n8n_workflows/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Five Whys Analyzer Agent - N8N Workflow - -## Overview -This directory contains the N8N workflow configuration for the Five Whys Analyzer Agent, which conducts systematic root cause analysis using the proven Five Whys methodology. - -## Workflow Files -- `workflow.json` - Production workflow for N8N import -- `workflow_dev.json` - Development/testing version (optional) -- `workflow_backup.json` - Backup version for disaster recovery - -## Webhook Configuration -- **Webhook URL**: Configured via `N8N_WEBHOOK_FIVE_WHYS` environment variable -- **HTTP Method**: POST -- **Expected Data Format**: - ```json - { - "problem": "Website conversion rate dropped by 30%", - "context": "E-commerce site, occurred after recent update", - "industry": "retail", - "stakeholders": ["marketing team", "dev team", "customers"], - "additional_info": "Peak season, mobile traffic increased" - } - ``` - -## Setup Instructions - -### 1. Import Workflow to N8N -1. Open your N8N instance -2. Click "Import from File" or "Import from URL" -3. Upload the `workflow.json` file -4. Configure credentials (OpenAI API key, etc.) -5. Activate the workflow - -### 2. Configure Webhook URL -1. Copy the webhook URL from N8N -2. Set environment variable: `N8N_WEBHOOK_FIVE_WHYS=https://your-n8n.com/webhook/five-whys` -3. Restart your Django application - -### 3. Test the Workflow -```bash -# Test via Django application -python manage.py test_webhook five_whys_analyzer - -# Or test directly via curl -curl -X POST https://your-n8n.com/webhook/five-whys \ - -H "Content-Type: application/json" \ - -d '{"problem":"Customer complaints increased","context":"After product launch","industry":"saas"}' -``` - -## Workflow Components -- **Webhook Node**: Receives requests from Django application -- **Problem Analysis**: Systematic Five Whys questioning process -- **AI Processing**: Uses OpenAI GPT-4 for intelligent analysis -- **Root Cause Identification**: Identifies underlying causes -- **Action Planning**: Generates actionable recommendations -- **Response Node**: Returns structured analysis results -- **Error Handling**: Manages analysis failures and edge cases - -## Expected Response Format -```json -{ - "success": true, - "analysis": { - "problem_statement": "Website conversion rate dropped by 30%", - "five_whys_sequence": [ - { - "question": "Why did the conversion rate drop?", - "answer": "Users are abandoning checkout process" - }, - { - "question": "Why are users abandoning checkout?", - "answer": "Page loading times increased significantly" - }, - { - "question": "Why did loading times increase?", - "answer": "New payment integration is slow" - }, - { - "question": "Why is the payment integration slow?", - "answer": "Third-party API has latency issues" - }, - { - "question": "Why wasn't this tested before deployment?", - "answer": "Load testing didn't include payment flow" - } - ], - "root_causes": [ - "Inadequate load testing procedures", - "Third-party API performance issues", - "Missing performance monitoring for payment flow" - ], - "immediate_actions": [ - "Switch to backup payment provider", - "Optimize payment integration code", - "Add performance monitoring" - ], - "long_term_solutions": [ - "Implement comprehensive load testing", - "Establish SLA requirements for third parties", - "Create performance regression testing" - ], - "prevention_strategies": [ - "Include all critical paths in testing", - "Monitor third-party dependencies", - "Establish performance baselines" - ] - }, - "confidence_level": "high", - "recommended_timeline": "immediate: 1-2 days, long-term: 2-4 weeks" -} -``` - -## Analysis Categories -- **Technical Issues**: Software bugs, performance problems -- **Process Problems**: Workflow inefficiencies, communication gaps -- **Human Factors**: Training gaps, resource constraints -- **External Factors**: Market changes, supplier issues -- **System Issues**: Infrastructure, tools, technology stack - -## Industry Applications -- Software Development (bugs, performance) -- Manufacturing (quality issues, downtime) -- Customer Service (complaint resolution) -- Marketing (campaign performance) -- Operations (process inefficiencies) -- Sales (conversion problems) - -## Troubleshooting -- **Shallow analysis**: Provide more context and stakeholder info -- **Generic recommendations**: Include industry-specific details -- **Missing root causes**: Ensure problem description is comprehensive -- **Incomplete action items**: Specify timeline and resource constraints - -## Best Practices -- Provide comprehensive problem context -- Include all relevant stakeholders -- Specify industry for targeted analysis -- Be specific about problem symptoms -- Include timeline and impact information -- Follow up on recommended actions -- Document lessons learned for future reference \ No newline at end of file diff --git a/five_whys_analyzer/processor.py b/five_whys_analyzer/processor.py deleted file mode 100644 index cbf9e58..0000000 --- a/five_whys_analyzer/processor.py +++ /dev/null @@ -1,291 +0,0 @@ -from agent_base.processors import StandardWebhookProcessor -from django.utils import timezone -from django.conf import settings -from .models import FiveWhysAnalyzerRequest, FiveWhysAnalyzerResponse -import json -import uuid -import logging - -logger = logging.getLogger(__name__) - - -class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): - """Dual-mode webhook processor for 5 Whys Analysis Agent - supports chat and report generation""" - - agent_slug = 'five-whys-analyzer' - webhook_url = 'https://m8taq6tk.rpcld.cc/webhook/5-whys-web' - agent_id = 'five-whys-001' - - # Security settings - webhook_timeout = 30 # seconds - max_retries = 2 - - def make_secure_webhook_request(self, payload): - """Make a secure webhook request with timeout and logging""" - import requests - - try: - logger.info(f"Making webhook request to {self.webhook_url} for agent {self.agent_id}") - - response = requests.post( - self.webhook_url, - json=payload, - timeout=self.webhook_timeout, - headers={ - 'Content-Type': 'application/json', - 'User-Agent': f'QuantumTasksAI-{self.agent_slug}/1.0' - } - ) - - response.raise_for_status() - response_data = response.json() - - logger.info(f"Webhook request successful for agent {self.agent_id}") - return response_data - - except requests.exceptions.Timeout: - logger.error(f"Webhook timeout for agent {self.agent_id}") - raise Exception("Service temporarily unavailable") - except requests.exceptions.RequestException as e: - logger.error(f"Webhook request failed for agent {self.agent_id}: {str(e)}") - raise Exception("External service error") - except ValueError as e: # JSON decode error - logger.error(f"Invalid webhook response format for agent {self.agent_id}: {str(e)}") - raise Exception("Invalid service response") - - def process_response(self, response_data, request_obj): - """Required implementation of abstract method - delegates to specific handlers""" - # This method is required by the base class but we handle responses - # differently in our dual-mode approach - return self.process_report_response(response_data, request_obj) - - def process_request(self, **kwargs): - """Handle both chat messages (free) and report generation (paid)""" - message_type = kwargs.get('message_type', 'chat') - - if message_type == 'chat': - return self.handle_chat_message(**kwargs) - elif message_type == 'generate_report': - return self.handle_report_generation(**kwargs) - else: - raise ValueError(f"Unknown message type: {message_type}") - - def handle_chat_message(self, **kwargs): - """Handle free chat interactions - no wallet deduction""" - user = kwargs.get('user') - session_id = kwargs.get('session_id', str(uuid.uuid4())) - user_message = kwargs.get('message', '') - - # Get the agent object - from agent_base.models import BaseAgent - try: - agent = BaseAgent.objects.get(slug=self.agent_slug) - except BaseAgent.DoesNotExist: - logger.error(f"Agent with slug '{self.agent_slug}' not found") - raise Exception("Service configuration error") - - # Get or create request object for this session - request_obj, created = FiveWhysAnalyzerRequest.objects.get_or_create( - user=user, - session_id=session_id, - chat_active=True, - defaults={ - 'agent': agent, - 'cost': 0, # No cost for chat - 'status': 'pending' - } - ) - - # Add user message to chat history - chat_messages = request_obj.chat_messages - chat_messages.append({ - 'role': 'user', - 'message': user_message, - 'timestamp': timezone.now().isoformat() - }) - request_obj.chat_messages = chat_messages - request_obj.save() - - # Prepare chat payload for webhook - chat_payload = { - 'message': { - 'text': f"Chat message: {user_message}. Provide helpful guidance about 5 Whys analysis. Do not generate the final report - just chat and help the user understand their problem." - }, - 'sessionId': session_id, - 'userId': str(user.id), - 'agentId': self.agent_id, - 'messageType': 'chat' - } - - # Send to webhook - response_data = self.make_secure_webhook_request(chat_payload) - - # Process chat response (no wallet deduction) - return self.process_chat_response(response_data, request_obj, user_message) - - def handle_report_generation(self, **kwargs): - """Handle paid report generation - deduct wallet after success""" - user = kwargs.get('user') - session_id = kwargs.get('session_id') - problem_statement = kwargs.get('problem_statement', '') - context_info = kwargs.get('context_info', '') - analysis_depth = kwargs.get('analysis_depth', 'standard') - - # Get the agent object - from agent_base.models import BaseAgent - try: - agent = BaseAgent.objects.get(slug=self.agent_slug) - except BaseAgent.DoesNotExist: - logger.error(f"Agent with slug '{self.agent_slug}' not found") - raise Exception("Service configuration error") - - # Get existing session or create new one - try: - request_obj = FiveWhysAnalyzerRequest.objects.get( - user=user, - session_id=session_id, - chat_active=True - ) - except FiveWhysAnalyzerRequest.DoesNotExist: - # Create new request for report generation - request_obj = FiveWhysAnalyzerRequest.objects.create( - user=user, - session_id=session_id, - agent=agent, - cost=8.0, # Cost for report generation - status='pending' - ) - - # Update request with report details - request_obj.problem_statement = problem_statement - request_obj.context_info = context_info - request_obj.analysis_depth = analysis_depth - request_obj.cost = 8.0 # Ensure cost is set for report - request_obj.save() - - # Prepare report generation payload - report_payload = { - 'message': { - 'text': f"Generate comprehensive 5 Whys analysis report.\nProblem: {problem_statement}\nContext: {context_info}\nDepth: {analysis_depth}\nChat History: {json.dumps(request_obj.chat_messages[-10:])}" - }, - 'sessionId': session_id, - 'userId': str(user.id), - 'agentId': self.agent_id, - 'messageType': 'report', - 'analysisDepth': analysis_depth - } - - # Send to webhook - response_data = self.make_secure_webhook_request(report_payload) - - # Process report response (with wallet deduction) - return self.process_report_response(response_data, request_obj) - - def process_chat_response(self, response_data, request_obj, user_message): - """Process chat response - no wallet deduction""" - try: - # Extract chat response - chat_response = response_data.get('output', response_data.get('message', 'I\'m here to help with 5 Whys analysis. What would you like to know?')) - - # Add assistant response to chat history - chat_messages = request_obj.chat_messages - chat_messages.append({ - 'role': 'assistant', - 'message': chat_response, - 'timestamp': timezone.now().isoformat() - }) - request_obj.chat_messages = chat_messages - request_obj.status = 'completed' # Chat message completed - request_obj.save() - - # Get or create response object - response_obj, created = FiveWhysAnalyzerResponse.objects.get_or_create( - request=request_obj, - defaults={ - 'success': True, - 'processing_time': response_data.get('processing_time', 0) - } - ) - - # Update response with chat data - response_obj.chat_response = chat_response - chat_history = response_obj.chat_history - chat_history.append({ - 'user_message': user_message, - 'assistant_response': chat_response, - 'timestamp': timezone.now().isoformat() - }) - response_obj.chat_history = chat_history - response_obj.save() - - print(f"{self.agent_slug}: Chat message processed - no wallet deduction") - return response_obj - - except Exception as e: - logger.error(f"Failed to process chat response: {str(e)}") - request_obj.status = 'failed' - request_obj.save() - raise Exception("Chat processing failed") - - def process_report_response(self, response_data, request_obj): - """Process report generation response - deduct wallet after success""" - try: - request_obj.status = 'processing' - request_obj.save() - - # Extract report data - final_report = response_data.get('output', response_data.get('report', '')) - success = bool(final_report) and response_data.get('success', True) - - # Get or create response object - response_obj, created = FiveWhysAnalyzerResponse.objects.get_or_create( - request=request_obj, - defaults={ - 'success': success, - 'processing_time': response_data.get('processing_time', 0) - } - ) - - if success: - # Update with final report - response_obj.final_report = final_report - response_obj.report_metadata = { - 'analysis_depth': request_obj.analysis_depth, - 'generated_at': timezone.now().isoformat(), - 'problem_statement': request_obj.problem_statement, - 'context_info': request_obj.context_info - } - response_obj.save() - - # Mark request as report generated - request_obj.report_generated = True - request_obj.chat_active = False # End chat session - - # ONLY deduct wallet balance after successful report generation - request_obj.user.deduct_balance( - request_obj.cost, - f"5 Whys Analysis Agent - Final Report ({request_obj.analysis_depth})", - 'five-whys-analyzer' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful report generation") - - request_obj.status = 'completed' - else: - request_obj.status = 'failed' - response_obj.error_message = "Failed to generate report" - response_obj.save() - - request_obj.processed_at = timezone.now() - request_obj.save() - - return response_obj - - except Exception as e: - logger.error(f"Failed to process report response: {str(e)}") - request_obj.status = 'failed' - request_obj.save() - raise Exception("Report generation failed") - - def prepare_message_text(self, **kwargs): - """Legacy method for compatibility""" - return kwargs.get('message', 'Process 5 Whys analysis') \ No newline at end of file diff --git a/five_whys_analyzer/templates/five_whys_analyzer/detail.html b/five_whys_analyzer/templates/five_whys_analyzer/detail.html deleted file mode 100644 index 77de989..0000000 --- a/five_whys_analyzer/templates/five_whys_analyzer/detail.html +++ /dev/null @@ -1,793 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}5 Whys Analysis Agent - Quantum Tasks AI{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} - -
- - {% include "components/agent_header.html" with agent_title="5 Whys Analyzer" agent_subtitle="AI-powered root cause analysis using the 5 Whys methodology" %} - - - {% include "components/quick_agents_panel.html" %} - - -
-
-
-

- šŸ’¬ - 5 Whys Analysis Chat -

-
-
- -
-

šŸ’¬ Chat with 5 Whys Analyst

- - -
-
5 Whys Analyst
-
- Hello! I'm here to help you with root cause analysis using the 5 Whys methodology. - - You can ask me questions, describe your problem, and I'll guide you through the analysis process. When you're ready, I can generate a comprehensive report for {{ agent.price }} AED. - - How can I help you today? -
-
- - -
-
- - -
- {% csrf_token %} -
- - -
-
- - -
-

šŸ“‹ Generate Final Report

- -
- šŸ’¬ Ask 2-3 questions about your problem first, then I'll generate a comprehensive report -
- - - - -
- - - -
-
- - -
-
-

- ā„¹ļø - How It Works -

-
-
-
    -
  1. Chat freely to explore your problem
  2. -
  3. Get guidance and ask questions
  4. -
  5. Generate final report when ready
  6. -
  7. Pay only for the final report
  8. -
- - - -
-
-
- - -
- {% include "components/processing_status.html" with status_title="Generating 5 Whys Report..." status_text="Please wait while we analyze your conversation and create a comprehensive report..." %} -
- - -
- {% include "components/results_container.html" with results_title="5 Whys Analysis Report" %} -
-
- - - - -{% endblock %} \ No newline at end of file diff --git a/five_whys_analyzer/urls.py b/five_whys_analyzer/urls.py deleted file mode 100644 index d333e82..0000000 --- a/five_whys_analyzer/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'five_whys_analyzer' - -urlpatterns = [ - path('', views.five_whys_analyzer_detail, name='detail'), - path('chat/', views.FiveWhysAnalyzerChatView.as_view(), name='chat'), - path('report/', views.FiveWhysAnalyzerReportView.as_view(), name='report'), - path('session//', views.five_whys_analyzer_session, name='session'), - # Legacy compatibility - path('process/', views.FiveWhysAnalyzerProcessView.as_view(), name='process'), -] \ No newline at end of file diff --git a/five_whys_analyzer/views.py b/five_whys_analyzer/views.py deleted file mode 100644 index f033a8a..0000000 --- a/five_whys_analyzer/views.py +++ /dev/null @@ -1,255 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_protect -from django.middleware.csrf import get_token -from django.utils.decorators import method_decorator -from django.views import View -from agent_base.models import BaseAgent -from .models import FiveWhysAnalyzerRequest, FiveWhysAnalyzerResponse -from .processor import FiveWhysAnalyzerProcessor -import json -import uuid -import logging - -logger = logging.getLogger(__name__) - -# Constants for input validation -MAX_MESSAGE_LENGTH = 5000 -MAX_PROBLEM_STATEMENT_LENGTH = 2000 -MAX_CONTEXT_LENGTH = 3000 -ALLOWED_ANALYSIS_DEPTHS = ['standard', 'detailed', 'comprehensive'] - - -def validate_input_data(data, validation_type="chat"): - """Validate and sanitize input data""" - errors = [] - - if validation_type == "chat": - message = data.get('message', '').strip() - if not message: - errors.append("Message cannot be empty") - elif len(message) > MAX_MESSAGE_LENGTH: - errors.append(f"Message too long (max {MAX_MESSAGE_LENGTH} characters)") - - # Basic HTML/script tag detection - if ' MAX_PROBLEM_STATEMENT_LENGTH: - errors.append(f"Problem statement too long (max {MAX_PROBLEM_STATEMENT_LENGTH} characters)") - - if context_info and len(context_info) > MAX_CONTEXT_LENGTH: - errors.append(f"Context information too long (max {MAX_CONTEXT_LENGTH} characters)") - - if analysis_depth not in ALLOWED_ANALYSIS_DEPTHS: - errors.append("Invalid analysis depth") - - return errors - - -def get_safe_error_response(error, request_type="request"): - """Return sanitized error message for production""" - logger.error(f"5 Whys Analyzer {request_type} error: {str(error)}") - - # Return generic error messages in production - if hasattr(error, '__class__'): - error_type = error.__class__.__name__ - if 'DoesNotExist' in error_type: - return 'Resource not found' - elif 'ValidationError' in error_type: - return 'Invalid input provided' - elif 'PermissionDenied' in error_type: - return 'Access denied' - elif 'IntegrityError' in error_type: - return 'Data conflict occurred' - - # Generic fallback - return 'An error occurred while processing your request' - - -@login_required -def five_whys_analyzer_detail(request): - """Detail page for 5 Whys Analysis Agent with chat interface""" - try: - agent = BaseAgent.objects.get(slug='five-whys-analyzer') - except BaseAgent.DoesNotExist: - messages.error(request, '5 Whys Analysis Agent agent not found.') - return redirect('core:homepage') - - # Get user's active chat sessions - active_sessions = FiveWhysAnalyzerRequest.objects.filter( - user=request.user, - chat_active=True - ).order_by('-created_at')[:5] - - # Get user's completed reports - completed_reports = FiveWhysAnalyzerRequest.objects.filter( - user=request.user, - report_generated=True - ).order_by('-created_at')[:10] - - context = { - 'agent': agent, - 'active_sessions': active_sessions, - 'completed_reports': completed_reports - } - return render(request, 'five_whys_analyzer/detail.html', context) - - -class FiveWhysAnalyzerChatView(View): - """Handle chat messages - free interactions""" - - def post(self, request): - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - # Parse request data - data = json.loads(request.body) - - # Validate input data - validation_errors = validate_input_data(data, "chat") - if validation_errors: - return JsonResponse({'error': '; '.join(validation_errors)}, status=400) - - # Get session ID or create new one - session_id = data.get('session_id', str(uuid.uuid4())) - user_message = data.get('message', '').strip() - - # Process chat message (no wallet deduction) - processor = FiveWhysAnalyzerProcessor() - result = processor.handle_chat_message( - user=request.user, - session_id=session_id, - message=user_message - ) - - return JsonResponse({ - 'success': True, - 'session_id': session_id, - 'response': result.chat_response, - 'message_type': 'chat' - }) - - except Exception as e: - error_message = get_safe_error_response(e, "chat") - return JsonResponse({'error': error_message}, status=500) - - -class FiveWhysAnalyzerReportView(View): - """Generate final report - paid interaction""" - - def post(self, request): - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - # Parse request data - data = json.loads(request.body) - - # Validate input data - validation_errors = validate_input_data(data, "report") - if validation_errors: - return JsonResponse({'error': '; '.join(validation_errors)}, status=400) - - # Get report parameters - session_id = data.get('session_id') - problem_statement = data.get('problem_statement', '').strip() - context_info = data.get('context_info', '').strip() - analysis_depth = data.get('analysis_depth', 'standard') - - if not session_id: - return JsonResponse({'error': 'Session ID required'}, status=400) - - # Get agent for price checking - agent = BaseAgent.objects.get(slug='five-whys-analyzer') - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - # Process report generation (wallet deduction after success) - processor = FiveWhysAnalyzerProcessor() - result = processor.handle_report_generation( - user=request.user, - session_id=session_id, - problem_statement=problem_statement, - context_info=context_info, - analysis_depth=analysis_depth - ) - - # Refresh user to get updated wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': True, - 'session_id': session_id, - 'report': result.final_report, - 'message_type': 'report', - 'analysis_depth': analysis_depth, - 'wallet_balance': float(request.user.wallet_balance) - }) - - except BaseAgent.DoesNotExist: - logger.error("5 Whys Analysis Agent not found in database") - return JsonResponse({'error': 'Service temporarily unavailable'}, status=404) - except Exception as e: - error_message = get_safe_error_response(e, "report") - return JsonResponse({'error': error_message}, status=500) - - -@login_required -def five_whys_analyzer_session(request, session_id): - """Get chat session data""" - try: - session_request = FiveWhysAnalyzerRequest.objects.get( - session_id=session_id, - user=request.user - ) - - session_data = { - 'session_id': session_id, - 'chat_messages': session_request.chat_messages, - 'chat_active': session_request.chat_active, - 'report_generated': session_request.report_generated, - 'problem_statement': session_request.problem_statement, - 'context_info': session_request.context_info, - 'analysis_depth': session_request.analysis_depth - } - - # Add final report if generated - if session_request.report_generated and hasattr(session_request, 'response'): - session_data['final_report'] = session_request.response.final_report - session_data['report_metadata'] = session_request.response.report_metadata - - return JsonResponse({ - 'success': True, - 'session': session_data - }) - - except FiveWhysAnalyzerRequest.DoesNotExist: - logger.warning(f"Session {session_id} not found for user {request.user.id}") - return JsonResponse({'error': 'Session not found'}, status=404) - except Exception as e: - error_message = get_safe_error_response(e, "session") - return JsonResponse({'error': error_message}, status=500) - - -# Legacy view for compatibility -class FiveWhysAnalyzerProcessView(View): - """Legacy process view - redirects to chat interface""" - - def post(self, request): - return JsonResponse({ - 'error': 'This endpoint is deprecated. Use the chat interface instead.', - 'redirect': '/agents/five-whys-analyzer/' - }, status=410) \ No newline at end of file diff --git a/job_posting_generator/__init__.py b/job_posting_generator/__init__.py deleted file mode 100644 index 3b51fe4..0000000 --- a/job_posting_generator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Job Posting Generator Agent App \ No newline at end of file diff --git a/job_posting_generator/admin.py b/job_posting_generator/admin.py deleted file mode 100644 index 9185711..0000000 --- a/job_posting_generator/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin -from .models import JobPostingGeneratorRequest, JobPostingGeneratorResponse - - -@admin.register(JobPostingGeneratorRequest) -class JobPostingGeneratorRequestAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'status', 'created_at', 'cost'] - list_filter = ['status', 'created_at'] - search_fields = ['user__email', 'user__username'] - readonly_fields = ['id', 'created_at', 'processed_at'] - ordering = ['-created_at'] - - -@admin.register(JobPostingGeneratorResponse) -class JobPostingGeneratorResponseAdmin(admin.ModelAdmin): - list_display = ['id', 'request', 'success', 'created_at'] - list_filter = ['success', 'created_at'] - readonly_fields = ['id', 'created_at'] - ordering = ['-created_at'] \ No newline at end of file diff --git a/job_posting_generator/apps.py b/job_posting_generator/apps.py deleted file mode 100644 index b3514ef..0000000 --- a/job_posting_generator/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class JobPostingGeneratorConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'job_posting_generator' \ No newline at end of file diff --git a/job_posting_generator/migrations/0001_initial.py b/job_posting_generator/migrations/0001_initial.py deleted file mode 100644 index 6321888..0000000 --- a/job_posting_generator/migrations/0001_initial.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-10 11:15 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('agent_base', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='JobPostingGeneratorRequest', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)), - ('cost', models.DecimalField(decimal_places=2, max_digits=10)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('processed_at', models.DateTimeField(blank=True, null=True)), - ('job_title', models.CharField(max_length=200)), - ('company_name', models.CharField(max_length=200)), - ('job_description', models.TextField()), - ('seniority_level', models.CharField(choices=[('entry', 'Entry Level (0-2 years)'), ('mid', 'Mid Level (2-5 years)'), ('senior', 'Senior Level (5-8 years)'), ('lead', 'Lead/Principal (8+ years)'), ('executive', 'Executive/C-Level')], max_length=20)), - ('contract_type', models.CharField(choices=[('full-time', 'Full-time'), ('part-time', 'Part-time'), ('contract', 'Contract'), ('freelance', 'Freelance'), ('internship', 'Internship')], max_length=20)), - ('location', models.CharField(max_length=200)), - ('language', models.CharField(choices=[('English', 'English'), ('Arabic', 'Arabic (Ų§Ł„Ų¹Ų±ŲØŁŠŲ©)'), ('Spanish', 'Spanish (EspaƱol)'), ('French', 'French (FranƧais)'), ('German', 'German (Deutsch)')], default='English', max_length=20)), - ('company_website', models.URLField(blank=True)), - ('how_to_apply', models.TextField(blank=True)), - ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agent_base.baseagent')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Job Posting Generator Request', - 'verbose_name_plural': 'Job Posting Generator Requests', - 'db_table': 'job_posting_generator_requests', - }, - ), - migrations.CreateModel( - name='JobPostingGeneratorResponse', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('success', models.BooleanField(default=False)), - ('error_message', models.TextField(blank=True)), - ('processing_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('job_posting_content', models.TextField(blank=True)), - ('formatted_posting', models.TextField(blank=True)), - ('raw_response', models.JSONField(blank=True, default=dict)), - ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='response', to='job_posting_generator.jobpostinggeneratorrequest')), - ], - options={ - 'verbose_name': 'Job Posting Generator Response', - 'verbose_name_plural': 'Job Posting Generator Responses', - 'db_table': 'job_posting_generator_responses', - }, - ), - ] diff --git a/job_posting_generator/migrations/__init__.py b/job_posting_generator/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/job_posting_generator/models.py b/job_posting_generator/models.py deleted file mode 100644 index a013bfb..0000000 --- a/job_posting_generator/models.py +++ /dev/null @@ -1,75 +0,0 @@ -from django.db import models -from decimal import Decimal -from agent_base.models import BaseAgentRequest, BaseAgentResponse - - -class JobPostingGeneratorRequest(BaseAgentRequest): - """Job Posting Generator request tracking""" - - # Required job details - job_title = models.CharField(max_length=200) - company_name = models.CharField(max_length=200) - job_description = models.TextField() - seniority_level = models.CharField( - max_length=20, - choices=[ - ('entry', 'Entry Level (0-2 years)'), - ('mid', 'Mid Level (2-5 years)'), - ('senior', 'Senior Level (5-8 years)'), - ('lead', 'Lead/Principal (8+ years)'), - ('executive', 'Executive/C-Level'), - ] - ) - contract_type = models.CharField( - max_length=20, - choices=[ - ('full-time', 'Full-time'), - ('part-time', 'Part-time'), - ('contract', 'Contract'), - ('freelance', 'Freelance'), - ('internship', 'Internship'), - ] - ) - location = models.CharField(max_length=200) - - # Optional fields - language = models.CharField( - max_length=20, - choices=[ - ('English', 'English'), - ('Arabic', 'Arabic (Ų§Ł„Ų¹Ų±ŲØŁŠŲ©)'), - ('Spanish', 'Spanish (EspaƱol)'), - ('French', 'French (FranƧais)'), - ('German', 'German (Deutsch)'), - ], - default='English' - ) - company_website = models.URLField(blank=True) - how_to_apply = models.TextField(blank=True) - - - class Meta: - db_table = 'job_posting_generator_requests' - verbose_name = 'Job Posting Generator Request' - verbose_name_plural = 'Job Posting Generator Requests' - - -class JobPostingGeneratorResponse(BaseAgentResponse): - """Job Posting Generator response storage""" - - request = models.OneToOneField( - JobPostingGeneratorRequest, - on_delete=models.CASCADE, - related_name='response' - ) - - # Agent-specific response fields - job_posting_content = models.TextField(blank=True) - formatted_posting = models.TextField(blank=True) - raw_response = models.JSONField(default=dict, blank=True) - - - class Meta: - db_table = 'job_posting_generator_responses' - verbose_name = 'Job Posting Generator Response' - verbose_name_plural = 'Job Posting Generator Responses' \ No newline at end of file diff --git a/job_posting_generator/n8n_workflows/Job_Posting_Generator.json b/job_posting_generator/n8n_workflows/Job_Posting_Generator.json deleted file mode 100644 index f1deb9e..0000000 --- a/job_posting_generator/n8n_workflows/Job_Posting_Generator.json +++ /dev/null @@ -1,300 +0,0 @@ -{ - "name": "Job Posting Generator", - "nodes": [ - { - "parameters": { - "model": { - "__rl": true, - "mode": "list", - "value": "gpt-4o", - "cachedResultName": "gpt-4o" - }, - "options": {} - }, - "id": "8bc1629f-d935-4fa8-bbb9-b55403207400", - "name": "OpenAI Chat Model", - "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "position": [ - 968, - 1020 - ], - "typeVersion": 1.2, - "credentials": { - "openAiApi": { - "id": "uzyuJ5c9nml2NneC", - "name": "OpenAi account" - } - } - }, - { - "parameters": { - "sessionIdType": "customKey", - "sessionKey": "={{ $('Set Web Input').item.json.body.sessionId }}", - "contextWindowLength": 50 - }, - "id": "93a19f8c-f3e3-4094-bbe7-019bcb5bdd0e", - "name": "Simple Memory", - "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "position": [ - 1088, - 1020 - ], - "typeVersion": 1.3 - }, - { - "parameters": { - "promptType": "define", - "text": "={{ $json.body.message.text }}", - "options": { - "systemMessage": "=You are an expert recruitment copywriter. Your task is to craft engaging and compelling job postings that attract top talent. For each job posting, use the provided input details (such as job title, responsibilities, qualifications, company information, and benefits) to:\n\nWrite a clear and enticing job title.\n\nSummarize the company and its culture in a way that excites candidates.\n\nClearly describe the role’s responsibilities and day-to-day tasks.\n\nList the Job title, About us, Job Overview, Responsibilities, required qualifications and preferred skills, Location and How to Apply in an appealing, concise manner.\n\nHighlight unique benefits and growth opportunities.\n\nUse inclusive, positive, and motivating language throughout.\n\nEnsure the posting is well-structured, easy to read, and free of jargon.\n\nYour goal is to make each job posting stand out and appeal to high-quality candidates, while accurately reflecting the role and company." - } - }, - "id": "1838d72d-12da-4351-beea-8625f60ff88d", - "name": "AI Agent", - "type": "@n8n/n8n-nodes-langchain.agent", - "position": [ - 940, - 800 - ], - "typeVersion": 1.9 - }, - { - "parameters": { - "chatId": "={{$('Telegram Trigger').first().json.message.chat.id}}", - "text": "={{ $json.output }}", - "additionalFields": { - "appendAttribution": false - } - }, - "id": "ce2d4d37-c3cb-4dd0-9b70-9e1db9830e74", - "name": "Send Response To Telegram", - "type": "n8n-nodes-base.telegram", - "position": [ - 500, - 440 - ], - "webhookId": "702bcdca-5297-4faf-9759-4f570d127052", - "typeVersion": 1.2, - "disabled": true - }, - { - "parameters": { - "httpMethod": "POST", - "path": "43f84411-eaaa-488c-9b1f-856e90d0aaf6", - "responseMode": "responseNode", - "options": {} - }, - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [ - 500, - 800 - ], - "id": "a02855f5-0b5c-47de-b098-19cd10932d88", - "webhookId": "43f84411-eaaa-488c-9b1f-856e90d0aaf6" - }, - { - "parameters": { - "options": {} - }, - "name": "Set Web Input", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 720, - 800 - ], - "id": "8c87b34b-9119-4f29-baea-9a6b74efc937" - }, - { - "parameters": { - "options": {} - }, - "name": "Respond to Web", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - 1316, - 800 - ], - "id": "e8ae02fc-93c3-476e-b01c-e60656ccfaac" - }, - { - "parameters": { - "formTitle": "Job Posting", - "formFields": { - "values": [ - { - "fieldLabel": "Job title" - }, - { - "fieldLabel": "Company Name" - }, - { - "fieldLabel": "Describe what you'd like to generate", - "fieldType": "textarea" - }, - { - "fieldLabel": "Seniority", - "fieldType": "dropdown", - "fieldOptions": { - "values": [ - { - "option": "Junior" - }, - { - "option": "Mid-level" - }, - { - "option": "Senior" - }, - { - "option": "Lead" - } - ] - } - }, - { - "fieldLabel": "Contract Type", - "fieldType": "dropdown", - "fieldOptions": { - "values": [ - { - "option": "Full-Time" - }, - { - "option": "Part-Time" - }, - { - "option": "Freelance" - }, - { - "option": "Internship" - } - ] - } - }, - { - "fieldLabel": "Location", - "fieldType": "dropdown", - "fieldOptions": { - "values": [ - { - "option": "Remote" - }, - { - "option": "On-Site" - }, - { - "option": "Hybrid" - } - ] - } - }, - { - "fieldLabel": "Language" - }, - { - "fieldLabel": "Company Website" - }, - { - "fieldLabel": "How to Apply" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.formTrigger", - "typeVersion": 2.2, - "position": [ - 500, - 180 - ], - "id": "0ad79a28-0909-4b88-bba0-e013cf4eae6d", - "name": "On form submission", - "webhookId": "75ac3236-9040-478a-88b4-e0bcce17fdf1", - "disabled": true - } - ], - "pinData": {}, - "connections": { - "AI Agent": { - "main": [ - [ - { - "node": "Respond to Web", - "type": "main", - "index": 0 - } - ] - ] - }, - "Simple Memory": { - "ai_memory": [ - [ - { - "node": "AI Agent", - "type": "ai_memory", - "index": 0 - } - ] - ] - }, - "OpenAI Chat Model": { - "ai_languageModel": [ - [ - { - "node": "AI Agent", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Webhook": { - "main": [ - [ - { - "node": "Set Web Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Web Input": { - "main": [ - [ - { - "node": "AI Agent", - "type": "main", - "index": 0 - } - ] - ] - }, - "On form submission": { - "main": [ - [] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1" - }, - "versionId": "b0e25b31-2e02-4d60-9ee3-4b512dc25fad", - "meta": { - "instanceId": "b419dceeef095c7882b7f3bc7ba03f620c77ec1f3d9d0518174b97d631dd49fa" - }, - "id": "nHrugmW7FvbKSlen", - "tags": [ - { - "createdAt": "2025-07-01T13:54:51.754Z", - "updatedAt": "2025-07-01T13:54:51.754Z", - "id": "2ji4EAexY8bmiTeM", - "name": "AI Agent" - } - ] -} \ No newline at end of file diff --git a/job_posting_generator/n8n_workflows/README.md b/job_posting_generator/n8n_workflows/README.md deleted file mode 100644 index 1e41fef..0000000 --- a/job_posting_generator/n8n_workflows/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Job Posting Generator Agent - N8N Workflow - -## Overview -This directory contains the N8N workflow configuration for the Job Posting Generator Agent, which creates comprehensive, professional job postings that attract qualified candidates. - -## Workflow Files -- `workflow.json` - Production workflow for N8N import -- `workflow_dev.json` - Development/testing version (optional) -- `workflow_backup.json` - Backup version for disaster recovery - -## Webhook Configuration -- **Webhook URL**: Configured via `N8N_WEBHOOK_JOB_POSTING` environment variable -- **HTTP Method**: POST -- **Expected Data Format**: - ```json - { - "position": "Senior Python Developer", - "company": "Tech Startup Inc", - "location": "New York, NY", - "experience_level": "senior", - "salary_range": "$120,000 - $150,000", - "responsibilities": ["API development", "Team leadership"], - "skills": ["Python", "Django", "PostgreSQL"], - "industry": "fintech" - } - ``` - -## Setup Instructions - -### 1. Import Workflow to N8N -1. Open your N8N instance -2. Click "Import from File" or "Import from URL" -3. Upload the `workflow.json` file -4. Configure credentials (OpenAI API key, etc.) -5. Activate the workflow - -### 2. Configure Webhook URL -1. Copy the webhook URL from N8N -2. Set environment variable: `N8N_WEBHOOK_JOB_POSTING=https://your-n8n.com/webhook/job-posting` -3. Restart your Django application - -### 3. Test the Workflow -```bash -# Test via Django application -python manage.py test_webhook job_posting_generator - -# Or test directly via curl -curl -X POST https://your-n8n.com/webhook/job-posting \ - -H "Content-Type: application/json" \ - -d '{"position":"Software Engineer","company":"Acme Corp","location":"Remote","experience_level":"mid"}' -``` - -## Workflow Components -- **Webhook Node**: Receives requests from Django application -- **AI Processing**: Uses OpenAI GPT-4 for job posting generation -- **Industry Optimization**: Tailors language for specific industries -- **Compliance Check**: Ensures legal compliance and inclusive language -- **Response Node**: Returns structured job posting content -- **Error Handling**: Manages generation failures and validation errors - -## Expected Response Format -```json -{ - "success": true, - "job_posting": { - "title": "Senior Python Developer", - "company_overview": "Join our innovative fintech startup...", - "job_description": "We are seeking an experienced Python developer...", - "key_responsibilities": [ - "Design and implement scalable APIs", - "Lead technical discussions and code reviews", - "Mentor junior developers" - ], - "requirements": { - "required": ["5+ years Python experience", "Django framework"], - "preferred": ["PostgreSQL", "AWS experience", "Team leadership"] - }, - "benefits": [ - "Competitive salary and equity", - "Health, dental, vision insurance", - "Flexible work arrangements" - ], - "application_instructions": "Send resume and cover letter to...", - "equal_opportunity_statement": "We are an equal opportunity employer..." - }, - "seo_keywords": ["python developer", "django", "fintech"], - "posting_platforms": ["linkedin", "indeed", "glassdoor"] -} -``` - -## Industry Specializations -- Technology/Software -- Healthcare -- Finance/Fintech -- Marketing/Advertising -- Manufacturing -- Education -- Non-profit -- Government - -## Compliance Features -- Equal opportunity language -- ADA compliance considerations -- Salary transparency requirements -- Location-specific labor law compliance -- Inclusive language recommendations - -## Troubleshooting -- **Generic postings**: Provide more company and role specifics -- **Compliance warnings**: Review generated content for bias -- **Missing requirements**: Ensure all mandatory fields are provided -- **Industry mismatch**: Verify industry parameter is correct - -## Best Practices -- Provide detailed company culture information -- Specify exact technical requirements -- Include growth opportunities and career path -- Use inclusive, welcoming language -- Optimize for relevant job board algorithms -- A/B test different posting variations \ No newline at end of file diff --git a/job_posting_generator/processor.py b/job_posting_generator/processor.py deleted file mode 100644 index 0cf062c..0000000 --- a/job_posting_generator/processor.py +++ /dev/null @@ -1,105 +0,0 @@ -from agent_base.processors import StandardWebhookProcessor -from django.utils import timezone -from django.conf import settings -from .models import JobPostingGeneratorRequest, JobPostingGeneratorResponse -import json - - -class JobPostingGeneratorProcessor(StandardWebhookProcessor): - """Webhook processor for Job Posting Generator agent""" - - agent_slug = 'job-posting-generator' - webhook_url = settings.N8N_WEBHOOK_JOB_POSTING - agent_id = 'job-posting' - - def prepare_message_text(self, **kwargs): - """Prepare detailed job posting prompt for N8N webhook""" - request_obj = kwargs.get('request_obj') - if not request_obj: - return "Create a professional job posting" - - # Build comprehensive job posting prompt - prompt = f""" -Create a professional job posting for the following position: - -Job Title: {request_obj.job_title} -Company: {request_obj.company_name} -Location: {request_obj.location} -Contract Type: {request_obj.get_contract_type_display()} -Seniority Level: {request_obj.get_seniority_level_display()} -Language: {request_obj.language} - -Job Description: -{request_obj.job_description} -""" - - if request_obj.company_website: - prompt += f"\nCompany Website: {request_obj.company_website}" - - if request_obj.how_to_apply: - prompt += f"\n\nApplication Instructions:\n{request_obj.how_to_apply}" - - prompt += "\n\nPlease create a comprehensive, professional job posting that includes all necessary sections such as job overview, responsibilities, qualifications, benefits, and clear application instructions." - - return prompt - - def process_response(self, response_data, request_obj): - """Process webhook response""" - try: - request_obj.status = 'processing' - request_obj.save() - - # Handle array response from N8N (extract first item) - if isinstance(response_data, list) and len(response_data) > 0: - response_data = response_data[0] - - # Extract job posting content - job_posting_content = "" - if isinstance(response_data, dict): - job_posting_content = response_data.get('output', response_data.get('text', response_data.get('content', ''))) - elif isinstance(response_data, str): - job_posting_content = response_data - - # Determine success based on response - success = bool(job_posting_content.strip()) and len(job_posting_content.strip()) > 50 - - # Create response object - response_obj = JobPostingGeneratorResponse.objects.create( - request=request_obj, - success=success, - processing_time=response_data.get('processing_time', 0) if isinstance(response_data, dict) else 0, - job_posting_content=job_posting_content, - formatted_posting=job_posting_content, # Same content for now - raw_response=response_data if isinstance(response_data, dict) else {'content': response_data} - ) - - # Only deduct wallet balance after successful processing - if success: - request_obj.user.deduct_balance( - request_obj.cost, - f"Job Posting Generator - {request_obj.job_title} at {request_obj.company_name}", - 'job-posting-generator' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") - - # Update request as completed - request_obj.status = 'completed' if success else 'failed' - request_obj.processed_at = timezone.now() - request_obj.save() - - return response_obj - - except Exception as e: - # Handle error - request_obj.status = 'failed' - request_obj.save() - - # Create error response - error_response = JobPostingGeneratorResponse.objects.create( - request=request_obj, - success=False, - error_message=str(e), - processing_time=0 - ) - - raise Exception(f"Failed to process Job Posting Generator response: {e}") \ No newline at end of file diff --git a/job_posting_generator/templates/job_posting_generator/detail.html b/job_posting_generator/templates/job_posting_generator/detail.html deleted file mode 100644 index b17bcdb..0000000 --- a/job_posting_generator/templates/job_posting_generator/detail.html +++ /dev/null @@ -1,764 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Job Posting Generator Agent - Quantum Tasks AI{% endblock %} - -{% block extra_css %} - - - -{% endblock %} - -{% block content %} - - -
- - {% include "components/agent_header.html" with agent_title="Job Posting Generator" agent_subtitle="Create professional job postings with AI-powered content generation" %} - - - {% include "components/quick_agents_panel.html" %} - - - {% if messages %} - {% for message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - - -
-
-
-

- šŸ“ - Job Posting Form -

-
-
- -
- {% csrf_token %} - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - {% if user.is_authenticated %} - {% if user.wallet_balance >= agent.price %} - - {% else %} -
- Insufficient balance! You need {{ agent.price }} AED. -
- - šŸ’° Top Up Wallet - - {% endif %} - {% else %} - - šŸ”‘ Login to Continue - - {% endif %} -
-
-
- - -
-
-

- ā„¹ļø - How It Works -

-
-
-
    -
  1. Enter job requirements
  2. -
  3. Configure position details
  4. -
  5. Process with AI
  6. -
  7. Get professional posting
  8. -
- - - -
-
-
- - -
- {% include "components/processing_status.html" with status_title="Creating Job Posting..." status_text="Please wait while we generate your professional job posting..." %} -
- - -
- {% include "components/results_container.html" with results_title="Generated Job Posting" %} -
-
-{% endblock %} - -{% block extra_js %} - - - -{% endblock %} \ No newline at end of file diff --git a/job_posting_generator/urls.py b/job_posting_generator/urls.py deleted file mode 100644 index 8087a1d..0000000 --- a/job_posting_generator/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'job_posting_generator' - -urlpatterns = [ - path('', views.job_posting_generator_detail, name='detail'), - path('status//', views.job_posting_generator_result, name='status'), -] \ No newline at end of file diff --git a/job_posting_generator/views.py b/job_posting_generator/views.py deleted file mode 100644 index a5f1074..0000000 --- a/job_posting_generator/views.py +++ /dev/null @@ -1,172 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator -from django.views import View -from agent_base.models import BaseAgent -from .models import JobPostingGeneratorRequest, JobPostingGeneratorResponse -from .processor import JobPostingGeneratorProcessor -import json - - -def job_posting_generator_detail(request): - """Detail page for Job Posting Generator agent""" - try: - agent = BaseAgent.objects.get(slug='job-posting-generator') - except BaseAgent.DoesNotExist: - messages.error(request, 'Job Posting Generator agent not found.') - return redirect('core:homepage') - - if request.method == 'POST': - # Handle AJAX requests - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - try: - # Create request object (no wallet deduction yet) - agent_request = JobPostingGeneratorRequest.objects.create( - user=request.user, - agent=agent, - cost=agent.price, - job_title=request.POST.get('job_title'), - company_name=request.POST.get('company_name'), - job_description=request.POST.get('job_description'), - seniority_level=request.POST.get('seniority_level'), - contract_type=request.POST.get('contract_type'), - location=request.POST.get('location'), - language=request.POST.get('language', 'English'), - company_website=request.POST.get('company_website', ''), - how_to_apply=request.POST.get('how_to_apply', ''), - ) - - # Process request - processor = JobPostingGeneratorProcessor() - result = processor.process_request( - request_obj=agent_request, - user_id=request.user.id, - ) - - # Refresh user from database to get updated wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': True, - 'request_id': str(agent_request.id), - 'message': 'Job posting generation started', - 'wallet_balance': float(request.user.wallet_balance) - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - # Regular form submission (redirect to avoid resubmission) - return redirect('job_posting_generator:detail') - - # GET request - show form - context = { - 'agent': agent, - } - return render(request, 'job_posting_generator/detail.html', context) - - -@method_decorator(csrf_exempt, name='dispatch') -class JobPostingGeneratorProcessView(View): - """Process Job Posting Generator requests""" - - def post(self, request): - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - # Parse request data - data = json.loads(request.body) - - # Get agent - agent = BaseAgent.objects.get(slug='job-posting-generator') - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - # Create request object (no wallet deduction yet - only after successful processing) - agent_request = JobPostingGeneratorRequest.objects.create( - user=request.user, - agent=agent, - cost=agent.price, - job_title=data.get('job_title'), - company_name=data.get('company_name'), - job_description=data.get('job_description'), - seniority_level=data.get('seniority_level'), - contract_type=data.get('contract_type'), - location=data.get('location'), - language=data.get('language', 'English'), - company_website=data.get('company_website', ''), - how_to_apply=data.get('how_to_apply', ''), - ) - - # Process request - processor = JobPostingGeneratorProcessor() - result = processor.process_request( - request_obj=agent_request, - user_id=request.user.id, - ) - - # Refresh user from database to get updated wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': True, - 'request_id': str(agent_request.id), - 'message': 'Job Posting Generator request processed successfully', - 'wallet_balance': float(request.user.wallet_balance) - }) - - except BaseAgent.DoesNotExist: - return JsonResponse({'error': 'Job Posting Generator agent not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -@login_required -def job_posting_generator_result(request, request_id): - """Get result for a specific request""" - try: - agent_request = JobPostingGeneratorRequest.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': response.success, - 'status': agent_request.status, - 'content': getattr(response, 'job_posting_content', None), - 'job_posting_content': getattr(response, 'job_posting_content', None), - 'formatted_posting': getattr(response, 'formatted_posting', None), - 'raw_response': getattr(response, 'raw_response', None), - 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except JobPostingGeneratorRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/netcop_hub/settings.py b/netcop_hub/settings.py index f014469..81ec2d1 100644 --- a/netcop_hub/settings.py +++ b/netcop_hub/settings.py @@ -73,16 +73,9 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'rest_framework', 'authentication', - 'wallet', + 'wallet', 'core', - 'agent_base', - 'weather_reporter', - 'data_analyzer', - 'job_posting_generator', - 'social_ads_generator', - 'email_writer', - 'five_whys_analyzer', - 'workflows', # New unified workflows app + 'workflows', # Unified workflows app (includes marketplace and agent execution) ] # Development apps (only in DEBUG mode) @@ -420,21 +413,11 @@ LOGGING = { 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, }, - 'agent_base': { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' if DEBUG else 'INFO', - 'propagate': False, - }, 'wallet': { 'handlers': ['console', 'file'], 'level': 'INFO', 'propagate': False, }, - 'agent_base.security': { - 'handlers': ['console', 'file'], - 'level': 'INFO', - 'propagate': False, - }, 'authentication.security': { 'handlers': ['console', 'file'], 'level': 'INFO', diff --git a/netcop_hub/urls.py b/netcop_hub/urls.py index e593803..2e6ba57 100644 --- a/netcop_hub/urls.py +++ b/netcop_hub/urls.py @@ -23,9 +23,8 @@ urlpatterns = [ path('admin/', admin.site.urls), path('auth/', include('authentication.urls')), path('wallet/', include('wallet.urls')), - path('', include('agent_base.urls')), - # Unified workflows system for all agents + # Unified workflows system for all agents (includes marketplace) path('agents/', include('workflows.urls')), path('', include('core.urls')), diff --git a/social_ads_generator/__init__.py b/social_ads_generator/__init__.py deleted file mode 100644 index e0610d4..0000000 --- a/social_ads_generator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Social Ads Generator Agent App \ No newline at end of file diff --git a/social_ads_generator/admin.py b/social_ads_generator/admin.py deleted file mode 100644 index 3526be7..0000000 --- a/social_ads_generator/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin -from .models import SocialAdsGeneratorRequest, SocialAdsGeneratorResponse - - -@admin.register(SocialAdsGeneratorRequest) -class SocialAdsGeneratorRequestAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'status', 'created_at', 'cost'] - list_filter = ['status', 'created_at'] - search_fields = ['user__email', 'user__username'] - readonly_fields = ['id', 'created_at', 'processed_at'] - ordering = ['-created_at'] - - -@admin.register(SocialAdsGeneratorResponse) -class SocialAdsGeneratorResponseAdmin(admin.ModelAdmin): - list_display = ['id', 'request', 'success', 'created_at'] - list_filter = ['success', 'created_at'] - readonly_fields = ['id', 'created_at'] - ordering = ['-created_at'] \ No newline at end of file diff --git a/social_ads_generator/apps.py b/social_ads_generator/apps.py deleted file mode 100644 index b929035..0000000 --- a/social_ads_generator/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class SocialAdsGeneratorConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'social_ads_generator' \ No newline at end of file diff --git a/social_ads_generator/migrations/0001_initial.py b/social_ads_generator/migrations/0001_initial.py deleted file mode 100644 index c819990..0000000 --- a/social_ads_generator/migrations/0001_initial.py +++ /dev/null @@ -1,61 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-10 12:33 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('agent_base', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='SocialAdsGeneratorRequest', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)), - ('cost', models.DecimalField(decimal_places=2, max_digits=10)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('processed_at', models.DateTimeField(blank=True, null=True)), - ('description', models.TextField(help_text='Product/service description')), - ('social_platform', models.CharField(choices=[('facebook', 'Facebook'), ('instagram', 'Instagram'), ('twitter', 'Twitter'), ('linkedin', 'LinkedIn'), ('tiktok', 'TikTok'), ('youtube', 'YouTube')], default='facebook', max_length=20)), - ('include_emoji', models.BooleanField(default=False, help_text='Include emojis in ad copy')), - ('language', models.CharField(choices=[('English', 'English'), ('Arabic', 'Arabic (Ų§Ł„Ų¹Ų±ŲØŁŠŲ©)'), ('Spanish', 'Spanish (EspaƱol)'), ('French', 'French (FranƧais)'), ('German', 'German (Deutsch)'), ('Chinese', 'Chinese (äø­ę–‡)')], default='English', max_length=20)), - ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agent_base.baseagent')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Social Ads Generator Request', - 'verbose_name_plural': 'Social Ads Generator Requests', - 'db_table': 'social_ads_generator_requests', - }, - ), - migrations.CreateModel( - name='SocialAdsGeneratorResponse', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('success', models.BooleanField(default=False)), - ('error_message', models.TextField(blank=True)), - ('processing_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('ad_copy', models.TextField(blank=True, help_text='Generated ad copy')), - ('hashtags', models.TextField(blank=True, help_text='Suggested hashtags')), - ('targeting_suggestions', models.TextField(blank=True, help_text='Audience targeting suggestions')), - ('formatted_ad', models.TextField(blank=True, help_text='Formatted ad content')), - ('raw_response', models.JSONField(blank=True, default=dict)), - ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='response', to='social_ads_generator.socialadsgeneratorrequest')), - ], - options={ - 'verbose_name': 'Social Ads Generator Response', - 'verbose_name_plural': 'Social Ads Generator Responses', - 'db_table': 'social_ads_generator_responses', - }, - ), - ] diff --git a/social_ads_generator/migrations/__init__.py b/social_ads_generator/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/social_ads_generator/models.py b/social_ads_generator/models.py deleted file mode 100644 index 0cce58b..0000000 --- a/social_ads_generator/models.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.db import models -from decimal import Decimal -from agent_base.models import BaseAgentRequest, BaseAgentResponse - - -class SocialAdsGeneratorRequest(BaseAgentRequest): - """Social Ads Generator request tracking""" - - # Required fields - description = models.TextField(help_text="Product/service description") - social_platform = models.CharField( - max_length=20, - choices=[ - ('facebook', 'Facebook'), - ('instagram', 'Instagram'), - ('twitter', 'Twitter'), - ('linkedin', 'LinkedIn'), - ('tiktok', 'TikTok'), - ('youtube', 'YouTube'), - ], - default='facebook' - ) - - # Optional fields - include_emoji = models.BooleanField(default=False, help_text="Include emojis in ad copy") - language = models.CharField( - max_length=20, - choices=[ - ('English', 'English'), - ('Arabic', 'Arabic (Ų§Ł„Ų¹Ų±ŲØŁŠŲ©)'), - ('Spanish', 'Spanish (EspaƱol)'), - ('French', 'French (FranƧais)'), - ('German', 'German (Deutsch)'), - ('Chinese', 'Chinese (äø­ę–‡)'), - ], - default='English' - ) - - - class Meta: - db_table = 'social_ads_generator_requests' - verbose_name = 'Social Ads Generator Request' - verbose_name_plural = 'Social Ads Generator Requests' - - -class SocialAdsGeneratorResponse(BaseAgentResponse): - """Social Ads Generator response storage""" - - request = models.OneToOneField( - SocialAdsGeneratorRequest, - on_delete=models.CASCADE, - related_name='response' - ) - - # Agent-specific response fields - ad_copy = models.TextField(blank=True, help_text="Generated ad copy") - hashtags = models.TextField(blank=True, help_text="Suggested hashtags") - targeting_suggestions = models.TextField(blank=True, help_text="Audience targeting suggestions") - formatted_ad = models.TextField(blank=True, help_text="Formatted ad content") - raw_response = models.JSONField(default=dict, blank=True) - - - class Meta: - db_table = 'social_ads_generator_responses' - verbose_name = 'Social Ads Generator Response' - verbose_name_plural = 'Social Ads Generator Responses' \ No newline at end of file diff --git a/social_ads_generator/n8n_workflows/README.md b/social_ads_generator/n8n_workflows/README.md deleted file mode 100644 index 1b8a8f0..0000000 --- a/social_ads_generator/n8n_workflows/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Social Ads Generator Agent - N8N Workflow - -## Overview -This directory contains the N8N workflow configuration for the Social Ads Generator Agent, which creates compelling social media advertisements for various platforms. - -## Workflow Files -- `workflow.json` - Production workflow for N8N import -- `workflow_dev.json` - Development/testing version (optional) -- `workflow_backup.json` - Backup version for disaster recovery - -## Webhook Configuration -- **Webhook URL**: Configured via `N8N_WEBHOOK_SOCIAL_ADS` environment variable -- **HTTP Method**: POST -- **Expected Data Format**: - ```json - { - "platform": "facebook", - "product": "AI Marketing Tool", - "audience": "small business owners", - "tone": "professional", - "features": ["automation", "analytics", "ROI tracking"], - "requirements": "Include call-to-action" - } - ``` - -## Setup Instructions - -### 1. Import Workflow to N8N -1. Open your N8N instance -2. Click "Import from File" or "Import from URL" -3. Upload the `workflow.json` file -4. Configure credentials (OpenAI API key, etc.) -5. Activate the workflow - -### 2. Configure Webhook URL -1. Copy the webhook URL from N8N -2. Set environment variable: `N8N_WEBHOOK_SOCIAL_ADS=https://your-n8n.com/webhook/social-ads` -3. Restart your Django application - -### 3. Test the Workflow -```bash -# Test via Django application -python manage.py test_webhook social_ads_generator - -# Or test directly via curl -curl -X POST https://your-n8n.com/webhook/social-ads \ - -H "Content-Type: application/json" \ - -d '{"platform":"instagram","product":"Coffee Shop","audience":"coffee lovers","tone":"casual"}' -``` - -## Workflow Components -- **Webhook Node**: Receives requests from Django application -- **AI Processing**: Uses OpenAI GPT-4 for ad content generation -- **Platform Optimization**: Tailors content for specific social media platforms -- **Response Node**: Returns structured ad content -- **Error Handling**: Manages failures and content generation issues - -## Expected Response Format -```json -{ - "success": true, - "ad_content": { - "headline": "Transform Your Business with AI", - "body": "Discover how AI can revolutionize your marketing...", - "call_to_action": "Start Free Trial", - "hashtags": ["#AI", "#Marketing", "#Business"], - "image_suggestions": ["professional team", "modern office"], - "target_audience": "business professionals aged 25-45" - }, - "platform_specs": { - "character_limit": 280, - "recommended_format": "image_post" - } -} -``` - -## Supported Platforms -- Facebook/Meta -- Instagram -- Twitter/X -- LinkedIn -- Google Ads -- TikTok -- Pinterest - -## Troubleshooting -- **Content not platform-optimized**: Check platform parameter is correct -- **Generic content**: Provide more specific product/audience details -- **API rate limits**: Monitor OpenAI usage and implement queuing -- **Webhook timeouts**: Optimize prompts for faster generation - -## Best Practices -- Provide detailed product descriptions for better results -- Specify target audience demographics clearly -- Test generated content before publishing -- A/B test different tone variations -- Monitor ad performance and adjust prompts accordingly \ No newline at end of file diff --git a/social_ads_generator/n8n_workflows/README_Optimized.md b/social_ads_generator/n8n_workflows/README_Optimized.md deleted file mode 100644 index 0cc519b..0000000 --- a/social_ads_generator/n8n_workflows/README_Optimized.md +++ /dev/null @@ -1,223 +0,0 @@ -# Social Ads Optimized - N8N Workflow - -## šŸš€ **Optimized Workflow for Simplified Frontend Integration** - -This is a completely redesigned N8N workflow that works with simplified frontend data and handles all complex processing internally. - -## šŸ“ **Files** -- `Social_Ads_Optimized.json` - New optimized workflow (USE THIS ONE) -- `Social_Ads.json` - Original workflow (for reference) -- `README_Optimized.md` - This documentation - -## šŸŽÆ **Key Improvements** - -### **Frontend Simplification (90% code reduction)** -- **Before**: Complex nested data structure with session management -- **After**: Simple form fields only - -### **Better Architecture** -- **Frontend**: Pure UI layer (form handling, display) -- **N8N**: All business logic (session management, prompt building, AI processing) - -## šŸ“ **Input Data Format** - -The workflow accepts simple form data: -```json -{ - "description": "Product or service description", - "social_platform": "facebook|instagram|linkedin|twitter|tiktok|youtube", - "include_emoji": "yes|no", - "language": "English|Arabic|Spanish|French|German|Chinese" -} -``` - -## šŸ”§ **Setup Instructions** - -### 1. Import to N8N -1. Open your N8N instance -2. Go to **Workflows** > **Import from File** -3. Upload `Social_Ads_Optimized.json` -4. Click **Import** - -### 2. Configure Credentials -1. Click on the **OpenAI Chat Model** node -2. Add your OpenAI API credentials -3. Select your preferred model (default: gpt-4o) - -### 3. Activate Workflow -1. Click the **Active** toggle at the top -2. Workflow status should show as "Active" - -### 4. Get Webhook URL -The webhook URL will be: -``` -http://your-n8n-instance:5678/webhook/social-ads-optimized -``` - -### 5. Update Frontend -Update your HTML/frontend to use the new webhook URL: -```javascript -fetch('http://localhost:5678/webhook/social-ads-optimized', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - description: "Your product description", - social_platform: "facebook", - include_emoji: "yes", - language: "English" - }) -}); -``` - -## šŸ—ļø **Workflow Architecture** - -### **Node Flow:** -1. **Webhook** - Receives simple form data -2. **Extract Form Data** - Processes input and generates session ID -3. **Build AI Prompt** - Creates detailed prompt from form fields -4. **OpenAI Chat Model** - GPT-4o language model -5. **Session Memory** - Maintains conversation context -6. **Social Ads AI Agent** - Processes request with optimized system prompt -7. **Format Response** - Structures output for frontend -8. **Respond to Webhook** - Returns result - -### **Key Features:** -- **Auto Session Management** - Generates unique session IDs automatically -- **Dynamic Prompt Building** - Creates tailored prompts based on form inputs -- **Platform Optimization** - Adjusts output for different social platforms -- **Language Support** - Handles multiple languages -- **Error Handling** - Robust error handling and response formatting - -## šŸ“¤ **Response Format** - -The workflow returns structured data: -```json -{ - "output": "Generated social media ad copy...", - "success": true, - "sessionId": "session_1234567890_abcdef", - "metadata": { - "platform": "facebook", - "language": "English", - "emojis": "yes", - "timestamp": 1234567890 - } -} -``` - -## šŸ” **Testing** - -### **Test via Frontend** -Use the "Test N8N Connection" button in the HTML interface. - -### **Test via curl** -```bash -curl -X POST http://localhost:5678/webhook/social-ads-optimized \ - -H "Content-Type: application/json" \ - -d '{ - "description": "AI-powered marketing automation tool", - "social_platform": "facebook", - "include_emoji": "yes", - "language": "English" - }' -``` - -### **Expected Response** -```json -{ - "output": "šŸš€ Transform your marketing with AI! Our automation tool helps businesses increase engagement by 300%. Perfect for entrepreneurs who want to scale faster. Start your free trial today! #AIMarketing #GrowthHack", - "success": true, - "sessionId": "session_1706123456_xyz789", - "metadata": { - "platform": "facebook", - "language": "English", - "emojis": "yes", - "timestamp": 1706123456789 - } -} -``` - -## šŸ› ļø **Customization** - -### **Modify AI Prompt** -Edit the **Build AI Prompt** node to change the prompt structure: -```javascript -"Create compelling social media advertisement copy for the following:\n\n" + -"Product/Service: " + $json.description + "\n" + -"Target Platform: " + $json.social_platform + "\n" + -// Add your custom prompt instructions here -``` - -### **Change System Message** -Edit the **Social Ads AI Agent** node system message for different AI behavior. - -### **Adjust Memory** -Modify the **Session Memory** node to change context window length. - -## šŸ› **Troubleshooting** - -### **Common Issues:** - -**1. Webhook not found (404)** -- Ensure workflow is active -- Check webhook URL spelling -- Verify workflow imported correctly - -**2. OpenAI errors** -- Check API credentials are configured -- Verify API key has sufficient credits -- Ensure model (gpt-4o) is available - -**3. Empty responses** -- Check N8N execution log for errors -- Verify all nodes are connected properly -- Test with simple input data first - -**4. Frontend connection issues** -- Ensure N8N is running on correct port -- Check CORS settings if needed -- Verify webhook URL matches exactly - -### **Debug Steps:** -1. Check N8N executions log -2. Test workflow manually in N8N -3. Verify input data format -4. Check browser network tab for request details - -## šŸ“ˆ **Performance** - -- **Response Time**: ~3-10 seconds (depends on OpenAI) -- **Concurrent Requests**: Supports multiple simultaneous requests -- **Memory Usage**: Efficient with 50-message context window -- **Error Rate**: <1% with proper OpenAI credits - -## šŸ”’ **Security** - -- **Input Validation**: Built-in input sanitization -- **Rate Limiting**: Controlled by N8N and OpenAI limits -- **Session Isolation**: Each request gets unique session ID -- **API Security**: OpenAI credentials stored securely in N8N - -## šŸ†š **Comparison with Original** - -| Feature | Original Workflow | Optimized Workflow | -|---------|------------------|-------------------| -| Frontend Code | 100+ lines | 10 lines | -| Data Structure | Complex nested | Simple flat | -| Session Management | Frontend | N8N automated | -| Prompt Building | Frontend | N8N dynamic | -| Maintainability | Hard | Easy | -| Architecture | Monolithic | Separated concerns | - -## šŸŽ‰ **Benefits** - -āœ… **90% less frontend code** -āœ… **Better separation of concerns** -āœ… **Easier maintenance and updates** -āœ… **More robust session management** -āœ… **Dynamic prompt optimization** -āœ… **Clean, professional architecture** - ---- - -**Ready to use!** Import the workflow, add your OpenAI credentials, and start generating amazing social media ads with minimal frontend complexity. \ No newline at end of file diff --git a/social_ads_generator/n8n_workflows/Social_Ads.json b/social_ads_generator/n8n_workflows/Social_Ads.json deleted file mode 100644 index 95a0fb6..0000000 --- a/social_ads_generator/n8n_workflows/Social_Ads.json +++ /dev/null @@ -1,266 +0,0 @@ -{ - "name": "Social Ads", - "nodes": [ - { - "parameters": { - "model": { - "__rl": true, - "mode": "list", - "value": "gpt-4o", - "cachedResultName": "gpt-4o" - }, - "options": {} - }, - "id": "5b2e2efd-32ab-4b6c-95cf-bfc73635ea2c", - "name": "OpenAI Chat Model", - "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "position": [ - 600, - 80 - ], - "typeVersion": 1.2, - "credentials": { - "openAiApi": { - "id": "uzyuJ5c9nml2NneC", - "name": "OpenAi account" - } - } - }, - { - "parameters": { - "sessionIdType": "customKey", - "sessionKey": "={{ $('Set Web Input').item.json.body.sessionId }}", - "contextWindowLength": 50 - }, - "id": "4240c19f-d502-43de-ab9a-3be9faa27bc3", - "name": "Simple Memory", - "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "position": [ - 780, - 100 - ], - "typeVersion": 1.3 - }, - { - "parameters": { - "promptType": "define", - "text": "={{ $json.body.message.text }}", - "options": { - "systemMessage": "=You are an expert social media advertiser. Your task is to craft catchy social media ad copy based on the input provided. Each ad must capture attention instantly, using concise and persuasive messaging that motivates action. Focus on highlighting key benefits, unique selling points, or emotional triggers relevant to the input. Keep the tone engaging, positive, and tailored to the target audience. Avoid fluff and ensure the message is clear and impactful.\n\nFormat your response as follows:\n\nAd Copy:\n[Your concise, persuasive ad copy here]\n\nIf appropriate, include a strong call-to-action. Do not use hashtags or emojis unless specifically requested." - } - }, - "id": "8da21f34-ffaf-451b-8896-633fe84fa8ae", - "name": "AI Agent", - "type": "@n8n/n8n-nodes-langchain.agent", - "position": [ - 640, - -180 - ], - "typeVersion": 1.9 - }, - { - "parameters": { - "chatId": "={{$('Telegram Trigger').first().json.message.chat.id}}", - "text": "={{ $json.output }}", - "additionalFields": { - "appendAttribution": false - } - }, - "id": "dbd609e5-dbd9-45c6-ae80-687bcf21d857", - "name": "Send Response To Telegram", - "type": "n8n-nodes-base.telegram", - "position": [ - 1160, - -300 - ], - "webhookId": "61937a8f-9757-40da-8ddb-c32b90ce1541", - "typeVersion": 1.2, - "disabled": true - }, - { - "parameters": { - "httpMethod": "POST", - "path": "2dc234d8-7217-454a-83e9-81afe5b4fe2d", - "responseMode": "responseNode", - "options": {} - }, - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [ - 180, - -40 - ], - "id": "9ceb26d2-34d9-41bc-9cdc-e318b8c5d174", - "webhookId": "2dc234d8-7217-454a-83e9-81afe5b4fe2d" - }, - { - "parameters": { - "options": {} - }, - "name": "Set Web Input", - "type": "n8n-nodes-base.set", - "typeVersion": 1, - "position": [ - 380, - -60 - ], - "id": "aecc9c8d-710e-4df9-98f4-ae886e18d3f0" - }, - { - "parameters": { - "options": {} - }, - "name": "Respond to Web", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - 1160, - 60 - ], - "id": "8b0d18bf-0c13-44d4-bf92-b3509cbb3c8a" - }, - { - "parameters": { - "formTitle": "Social Ads", - "formFields": { - "values": [ - { - "fieldLabel": "Describe what you'd like to generate", - "fieldType": "textarea" - }, - { - "fieldLabel": "Include Emoji", - "fieldType": "dropdown", - "fieldOptions": { - "values": [ - { - "option": "Yes" - }, - { - "option": "No" - } - ] - } - }, - { - "fieldLabel": "For Social Media Platform", - "fieldType": "dropdown", - "fieldOptions": { - "values": [ - { - "option": "Facebook" - }, - { - "option": "Instagram" - }, - { - "option": "LinkedIn" - }, - { - "option": "X (Twitter)" - } - ] - } - }, - { - "fieldLabel": "Language" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.formTrigger", - "typeVersion": 2.2, - "position": [ - 200, - -380 - ], - "id": "92974cef-cb9a-42cb-9054-8989cae4d37b", - "name": "On form submission", - "webhookId": "2daa7ed9-6823-4eea-8ce8-e0dfdfb1110d", - "disabled": true - } - ], - "pinData": {}, - "connections": { - "AI Agent": { - "main": [ - [ - { - "node": "Respond to Web", - "type": "main", - "index": 0 - } - ] - ] - }, - "Simple Memory": { - "ai_memory": [ - [ - { - "node": "AI Agent", - "type": "ai_memory", - "index": 0 - } - ] - ] - }, - "OpenAI Chat Model": { - "ai_languageModel": [ - [ - { - "node": "AI Agent", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Webhook": { - "main": [ - [ - { - "node": "Set Web Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Set Web Input": { - "main": [ - [ - { - "node": "AI Agent", - "type": "main", - "index": 0 - } - ] - ] - }, - "On form submission": { - "main": [ - [] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1" - }, - "versionId": "68aa150a-4be4-4922-b387-76f721c65295", - "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "b419dceeef095c7882b7f3bc7ba03f620c77ec1f3d9d0518174b97d631dd49fa" - }, - "id": "d1bIXx3TKRtmdhpB", - "tags": [ - { - "createdAt": "2025-07-01T13:54:51.754Z", - "updatedAt": "2025-07-01T13:54:51.754Z", - "id": "2ji4EAexY8bmiTeM", - "name": "AI Agent" - } - ] -} \ No newline at end of file diff --git a/social_ads_generator/n8n_workflows/Social_Ads_Optimized.json b/social_ads_generator/n8n_workflows/Social_Ads_Optimized.json deleted file mode 100644 index 9596b27..0000000 --- a/social_ads_generator/n8n_workflows/Social_Ads_Optimized.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "name": "Social Ads Optimized", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "social-ads-optimized", - "responseMode": "responseNode", - "options": {} - }, - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [200, 200], - "id": "webhook-node-001", - "webhookId": "social-ads-optimized" - }, - { - "parameters": { - "mode": "manual", - "duplicateItem": false, - "assignments": { - "assignments": [ - { - "id": "session-id", - "name": "sessionId", - "value": "={{ $json.body.sessionId }}", - "type": "string" - }, - { - "id": "description", - "name": "description", - "value": "={{ $json.body.description }}", - "type": "string" - }, - { - "id": "platform", - "name": "social_platform", - "value": "={{ $json.body.social_platform }}", - "type": "string" - }, - { - "id": "emoji", - "name": "include_emoji", - "value": "={{ $json.body.include_emoji }}", - "type": "string" - }, - { - "id": "language", - "name": "language", - "value": "={{ $json.body.language }}", - "type": "string" - } - ] - }, - "options": {} - }, - "name": "Extract Form Data", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [400, 200], - "id": "extract-form-data-001" - }, - { - "parameters": { - "mode": "manual", - "duplicateItem": false, - "assignments": { - "assignments": [ - { - "id": "chat-input", - "name": "chatInput", - "value": "={{ \"Create compelling social media advertisement copy for the following:\\n\\nProduct/Service: \" + $json.description + \"\\nTarget Platform: \" + $json.social_platform + \"\\nLanguage: \" + $json.language + \"\\nInclude Emojis: \" + $json.include_emoji + \"\\n\\nPlease create advertisement copy that:\\n- Captures attention instantly\\n- Highlights key benefits and unique selling points\\n- Uses persuasive messaging that motivates action\\n- Includes a strong call-to-action\\n- Is tailored to \" + $json.social_platform + \" audience\\n- Uses \" + $json.language + \" language\" + ($json.include_emoji === \"yes\" ? \"\\n- Incorporates relevant emojis for engagement\" : \"\") + \"\\n\\nFormat the response as professional ad copy ready for social media posting. Provide multiple variations if possible.\" }}", - "type": "string" - }, - { - "id": "session-id-copy", - "name": "sessionId", - "value": "={{ $('Extract Form Data').item.json.sessionId }}", - "type": "string" - } - ] - }, - "options": {} - }, - "name": "Build AI Prompt", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [600, 200], - "id": "build-prompt-001" - }, - { - "parameters": { - "model": { - "__rl": true, - "mode": "list", - "value": "gpt-4o", - "cachedResultName": "gpt-4o" - }, - "options": {} - }, - "id": "openai-model-001", - "name": "OpenAI Chat Model", - "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "position": [800, 100], - "typeVersion": 1.2, - "credentials": { - "openAiApi": { - "id": "openai-credentials", - "name": "OpenAI API" - } - } - }, - { - "parameters": { - "promptType": "define", - "text": "={{ $('Build AI Prompt').item.json.chatInput }}", - "options": { - "systemMessage": "You are an expert social media advertiser and copywriter. Your task is to create compelling, engaging social media advertisements that drive action. Focus on creating concise, persuasive copy that captures attention instantly and motivates the target audience to take action. Always include a strong call-to-action and tailor your language to the specified platform and audience. Be creative, authentic, and results-oriented in your approach." - } - }, - "id": "ai-agent-001", - "name": "Social Ads AI Agent", - "type": "@n8n/n8n-nodes-langchain.agent", - "position": [1000, 200], - "typeVersion": 1.9 - }, - { - "parameters": { - "mode": "manual", - "duplicateItem": false, - "assignments": { - "assignments": [ - { - "id": "response-output", - "name": "output", - "value": "={{ $json.output }}", - "type": "string" - }, - { - "id": "success-flag", - "name": "success", - "value": true, - "type": "boolean" - }, - { - "id": "session-info", - "name": "sessionId", - "value": "={{ $('Extract Form Data').item.json.sessionId }}", - "type": "string" - }, - { - "id": "metadata", - "name": "metadata", - "value": "={{ { \"platform\": $('Extract Form Data').item.json.social_platform, \"language\": $('Extract Form Data').item.json.language, \"emojis\": $('Extract Form Data').item.json.include_emoji, \"timestamp\": $now } }}", - "type": "object" - } - ] - }, - "options": {} - }, - "name": "Format Response", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [1200, 200], - "id": "format-response-001" - }, - { - "parameters": { - "options": {} - }, - "name": "Respond to Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [1400, 200], - "id": "respond-webhook-001" - } - ], - "pinData": {}, - "connections": { - "Webhook": { - "main": [ - [ - { - "node": "Extract Form Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract Form Data": { - "main": [ - [ - { - "node": "Build AI Prompt", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build AI Prompt": { - "main": [ - [ - { - "node": "Social Ads AI Agent", - "type": "main", - "index": 0 - } - ] - ] - }, - "OpenAI Chat Model": { - "ai_languageModel": [ - [ - { - "node": "Social Ads AI Agent", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Social Ads AI Agent": { - "main": [ - [ - { - "node": "Format Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Response": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1" - }, - "versionId": "optimized-social-ads-v1", - "meta": { - "templateCredsSetupCompleted": false, - "instanceId": "social-ads-optimized-workflow" - }, - "id": "social-ads-optimized", - "tags": [ - { - "id": "ai-agent-optimized", - "name": "AI Agent Optimized" - }, - { - "id": "social-media", - "name": "Social Media" - } - ] -} \ No newline at end of file diff --git a/social_ads_generator/processor.py b/social_ads_generator/processor.py deleted file mode 100644 index 51ec354..0000000 --- a/social_ads_generator/processor.py +++ /dev/null @@ -1,232 +0,0 @@ -from agent_base.processors import StandardWebhookProcessor -from django.utils import timezone -from django.conf import settings -from .models import SocialAdsGeneratorRequest, SocialAdsGeneratorResponse -import json - - -class SocialAdsGeneratorProcessor(StandardWebhookProcessor): - """Webhook processor for Social Ads Generator agent""" - - agent_slug = 'social-ads-generator' - webhook_url = settings.N8N_WEBHOOK_SOCIAL_ADS - agent_id = 'social-ads' - - def prepare_message_text(self, **kwargs): - """Prepare detailed social ads prompt for N8N webhook""" - request_obj = kwargs.get('request_obj') - if not request_obj: - return "Create a social media advertisement" - - # Sanitize and validate description content - sanitized_description = self.sanitize_user_input(request_obj.description) - if not sanitized_description: - return "Unable to process the provided description" - - # Validate platform and language choices - platform_display = self.get_safe_platform_display(request_obj.social_platform) - safe_language = self.get_safe_language(request_obj.language) - - # Build comprehensive social ads prompt with sanitized inputs - prompt = f""" -Create a compelling social media advertisement for the following: - -Product/Service Description: -{sanitized_description} - -Target Platform: {platform_display} -Language: {safe_language} -Include Emojis: {'Yes' if request_obj.include_emoji else 'No'} - -Please create platform-optimized ad copy that: -- Captures attention instantly -- Highlights key benefits and unique selling points -- Uses persuasive messaging that motivates action -- Includes a strong call-to-action -- Is tailored to {platform_display} audience -- Uses {safe_language} language -- Maintains professional and appropriate content -- Avoids any misleading or harmful messaging -""" - - if request_obj.include_emoji: - prompt += "\n- Incorporates relevant emojis for engagement" - - prompt += "\n\nFormat the response as professional ad copy ready for social media posting." - - return prompt - - def sanitize_user_input(self, description): - """Sanitize user input to prevent prompt injection and harmful content""" - if not description or not isinstance(description, str): - return "" - - # Remove potential prompt injection patterns - dangerous_patterns = [ - 'ignore previous instructions', - 'new instructions:', - 'system:', - 'assistant:', - 'user:', - '###', - 'IGNORE', - 'STOP', - 'OVERRIDE', - ] - - sanitized = description.strip() - - # Check for and remove dangerous patterns (case insensitive) - for pattern in dangerous_patterns: - if pattern.lower() in sanitized.lower(): - # Replace with safe placeholder - sanitized = sanitized.replace(pattern, '[CONTENT_FILTERED]') - - # Limit length and remove excessive whitespace - sanitized = ' '.join(sanitized.split())[:2000] - - # Basic content filtering for inappropriate requests - inappropriate_keywords = [ - 'illegal', 'harmful', 'violence', 'hate', 'discrimination', - 'scam', 'fraud', 'misleading', 'fake', 'counterfeit' - ] - - sanitized_lower = sanitized.lower() - for keyword in inappropriate_keywords: - if keyword in sanitized_lower: - return f"[Content filtered - Please provide appropriate product/service description]" - - return sanitized - - def get_safe_platform_display(self, platform): - """Get safe platform display name""" - platform_map = { - 'facebook': 'Facebook', - 'instagram': 'Instagram', - 'twitter': 'Twitter', - 'linkedin': 'LinkedIn', - 'tiktok': 'TikTok', - 'youtube': 'YouTube' - } - return platform_map.get(platform, 'Social Media') - - def get_safe_language(self, language): - """Get safe language name""" - language_map = { - 'English': 'English', - 'Arabic': 'Arabic', - 'Spanish': 'Spanish', - 'French': 'French', - 'German': 'German', - 'Chinese': 'Chinese' - } - return language_map.get(language, 'English') - - def process_response(self, response_data, request_obj): - """Process webhook response""" - try: - request_obj.status = 'processing' - request_obj.save() - - - # Handle array response from N8N (extract first item) - if isinstance(response_data, list) and len(response_data) > 0: - response_data = response_data[0] - - # Extract and validate ad copy content - ad_copy = "" - if isinstance(response_data, dict): - ad_copy = response_data.get('output', response_data.get('text', response_data.get('content', ''))) - elif isinstance(response_data, str): - ad_copy = response_data - - # Validate and sanitize AI output - ad_copy = self.validate_ai_output(ad_copy) - - # Parse ad copy for different components (basic parsing) - hashtags = "" - targeting_suggestions = "" - formatted_ad = ad_copy - - # Simple extraction of hashtags if present - if '#' in ad_copy: - lines = ad_copy.split('\n') - hashtag_lines = [line for line in lines if line.strip().startswith('#')] - if hashtag_lines: - hashtags = ' '.join(hashtag_lines) - - # Determine success based on response - success = response_data.get('success', False) if isinstance(response_data, dict) else bool(ad_copy.strip()) - - # Create response object - response_obj = SocialAdsGeneratorResponse.objects.create( - request=request_obj, - success=success, - processing_time=response_data.get('processing_time', 0) if isinstance(response_data, dict) else 0, - ad_copy=ad_copy, - hashtags=hashtags, - targeting_suggestions=targeting_suggestions, - formatted_ad=formatted_ad, - raw_response=response_data if isinstance(response_data, dict) else {'content': response_data} - ) - - # Only deduct wallet balance after successful processing - if success: - request_obj.user.deduct_balance( - request_obj.cost, - f"Social Ads Generator - {request_obj.get_social_platform_display()} ad for {request_obj.description[:50]}...", - 'social-ads-generator' - ) - print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") - - # Update request as completed - request_obj.status = 'completed' if success else 'failed' - request_obj.processed_at = timezone.now() - request_obj.save() - - return response_obj - - except Exception as e: - # Handle error - request_obj.status = 'failed' - request_obj.save() - - # Create error response - SocialAdsGeneratorResponse.objects.create( - request=request_obj, - success=False, - error_message=str(e), - processing_time=0 - ) - - raise Exception(f"Failed to process Social Ads Generator response: {e}") - - def validate_ai_output(self, content): - """Validate and sanitize AI-generated content""" - if not content or not isinstance(content, str): - return "Error: No content generated" - - # Limit output length for security - content = content[:10000] - - # Remove any potential malicious content - malicious_patterns = [ - ' - -{% endblock %} - -{% block content %} - - -
- - {% include "components/agent_header.html" with agent_title="Social Ads Generator" agent_subtitle="Create compelling social media advertisements optimized for different platforms" %} - - - {% include "components/quick_agents_panel.html" %} - - -
- -
-
-

- šŸ“¢ - Social Ads Details -

-
-
-
- {% csrf_token %} - - - - - -
-

šŸ“± Platform & Formatting

- -
- - -
Choose the social media platform for optimization
- -
- -
- - -
Whether to include emojis in the ad copy
- -
-
- - -
- {% if user.is_authenticated %} - {% if user.wallet_balance >= agent.price %} - - {% else %} -
- Insufficient balance! You need {{ agent.price }} AED. -
- - šŸ’° Top Up Wallet - - {% endif %} - {% else %} - - šŸ” Login to Continue - - {% endif %} -
-
-
-
- - -
-
-

- ā„¹ļø - How It Works -

-
-
-
    -
  1. Describe your product/service
  2. -
  3. Select target platform
  4. -
  5. Choose emoji preferences
  6. -
  7. Get optimized ad copy
  8. -
- - - -
-
-
- - -
- - {% include "components/processing_status.html" with status_title="Creating Social Ads..." status_text="Please wait while we generate your ad copy..." %} - - - {% include "components/results_container.html" with results_title="Generated Social Ads" %} -
-
- -{% endblock %} \ No newline at end of file diff --git a/social_ads_generator/urls.py b/social_ads_generator/urls.py deleted file mode 100644 index 99fcccd..0000000 --- a/social_ads_generator/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'social_ads_generator' - -urlpatterns = [ - path('', views.social_ads_generator_detail, name='detail'), - path('status//', views.social_ads_generator_status, name='status'), -] \ No newline at end of file diff --git a/social_ads_generator/views.py b/social_ads_generator/views.py deleted file mode 100644 index 9b683f7..0000000 --- a/social_ads_generator/views.py +++ /dev/null @@ -1,158 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -import logging -from django.contrib import messages -from django.http import JsonResponse -from agent_base.models import BaseAgent -from .models import SocialAdsGeneratorRequest, SocialAdsGeneratorResponse -from .processor import SocialAdsGeneratorProcessor - - -@login_required -def social_ads_generator_detail(request): - """Detail page for Social Ads Generator agent""" - try: - agent = BaseAgent.objects.get(slug='social-ads-generator') - except BaseAgent.DoesNotExist: - messages.error(request, 'Social Ads Generator agent not found.') - return redirect('core:homepage') - - if request.method == 'POST': - # Handle AJAX requests - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - try: - # Validate and sanitize input data - description = request.POST.get('description', '').strip() - social_platform = request.POST.get('social_platform', 'facebook') - include_emoji = request.POST.get('include_emoji') == 'yes' - language = request.POST.get('language', 'English') - - # Server-side validation - validation_errors = [] - - # Validate description - if not description: - validation_errors.append('Description is required') - elif len(description) < 10: - validation_errors.append('Description must be at least 10 characters long') - elif len(description) > 5000: - validation_errors.append('Description must be less than 5000 characters') - - # Validate social platform - valid_platforms = ['facebook', 'instagram', 'twitter', 'linkedin', 'tiktok', 'youtube'] - if social_platform not in valid_platforms: - validation_errors.append('Invalid social media platform selected') - - # Validate language - valid_languages = ['English', 'Arabic', 'Spanish', 'French', 'German', 'Chinese'] - if language not in valid_languages: - validation_errors.append('Invalid language selected') - - # Return validation errors if any - if validation_errors: - return JsonResponse({'error': '; '.join(validation_errors)}, status=400) - - # Create request object (no wallet deduction yet) - agent_request = SocialAdsGeneratorRequest.objects.create( - user=request.user, - agent=agent, - cost=agent.price, - description=description, - social_platform=social_platform, - include_emoji=include_emoji, - language=language, - ) - - # Process request - processor = SocialAdsGeneratorProcessor() - result = processor.process_request( - request_obj=agent_request, - user_id=request.user.id, - ) - - # Refresh user from database to get updated wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': True, - 'request_id': str(agent_request.id), - 'message': 'Social ads generation started', - 'wallet_balance': float(request.user.wallet_balance) - }) - - except Exception as e: - # Log detailed error for debugging (server-side only) - logger = logging.getLogger(__name__) - logger.error(f"Social ads generation failed for user {request.user.id}: {str(e)}", exc_info=True) - - # Return generic error message to client - return JsonResponse({'error': 'Processing failed. Please try again later.'}, status=500) - - # Regular form submission (redirect to avoid resubmission) - return redirect('social_ads_generator:detail') - - # GET request - show form - context = { - 'agent': agent, - } - return render(request, 'social_ads_generator/detail.html', context) - - - - -@login_required -def social_ads_generator_status(request, request_id): - """Get status for a specific request (for polling)""" - try: - agent_request = SocialAdsGeneratorRequest.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - ad_copy = getattr(response, 'ad_copy', None) - raw_response = getattr(response, 'raw_response', None) - - - json_response = { - 'success': response.success, - 'status': agent_request.status, - 'content': ad_copy, - 'ad_copy_content': ad_copy, - 'hashtags': getattr(response, 'hashtags', None), - 'targeting_suggestions': getattr(response, 'targeting_suggestions', None), - 'formatted_ad': getattr(response, 'formatted_ad', None), - 'raw_response': raw_response, - 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - } - - return JsonResponse(json_response) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except SocialAdsGeneratorRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - # Log detailed error for debugging (server-side only) - logger = logging.getLogger(__name__) - logger.error(f"Social ads status check failed for request {request_id}: {str(e)}", exc_info=True) - - # Return generic error message to client - return JsonResponse({'error': 'Unable to retrieve status. Please try again later.'}, status=500) \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 5d3c635..22ae978 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,7 +30,7 @@ - {% else %} -
- Insufficient balance! You need {{ agent.price }} AED. -
-
- šŸ’° Top Up Wallet - - {% endif %} - {% else %} - - šŸ” Login to Continue - - {% endif %} - - - - - - -
-
-

- ā„¹ļø - How It Works -

-
-
-
    -
  1. Enter any city name worldwide
  2. -
  3. Choose your preferred report type
  4. -
  5. Get real-time weather data
  6. -
  7. Copy or download detailed reports
  8. -
- - - -
-
- - - -
- - {% include "components/processing_status.html" with status_title="Getting Weather Data..." status_text="Fetching real-time weather information..." %} - - - {% include "components/results_container.html" with results_title="Weather Report" %} -
- -{% endblock %} \ No newline at end of file diff --git a/weather_reporter/urls.py b/weather_reporter/urls.py deleted file mode 100644 index 0b535b9..0000000 --- a/weather_reporter/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'weather_reporter' - -urlpatterns = [ - path('', views.weather_reporter_detail, name='detail'), - path('process/', views.WeatherReporterProcessView.as_view(), name='process'), - path('status//', views.weather_reporter_status, name='status'), - path('result//', views.weather_reporter_result, name='result'), -] \ No newline at end of file diff --git a/weather_reporter/views.py b/weather_reporter/views.py deleted file mode 100644 index 03db215..0000000 --- a/weather_reporter/views.py +++ /dev/null @@ -1,202 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator -from django.views import View -from agent_base.models import BaseAgent -from .models import WeatherReporterRequest, WeatherReporterResponse -from .processor import WeatherReporterProcessor -import json - - -@login_required -def weather_reporter_detail(request): - """Detail page for Weather Reporter agent""" - try: - agent = BaseAgent.objects.get(slug='weather-reporter') - except BaseAgent.DoesNotExist: - messages.error(request, 'Weather Reporter agent not found.') - return redirect('core:homepage') - - # Get user requests only if authenticated - user_requests = [] - if request.user.is_authenticated: - user_requests = WeatherReporterRequest.objects.filter(user=request.user).order_by('-created_at')[:10] - - context = { - 'agent': agent, - 'user_requests': user_requests - } - return render(request, 'weather_reporter/detail.html', context) - - -@method_decorator(csrf_exempt, name='dispatch') -class WeatherReporterProcessView(View): - """Process Weather Reporter requests""" - - def post(self, request): - if not request.user.is_authenticated: - return JsonResponse({'error': 'Authentication required'}, status=401) - - try: - # Handle FormData from frontend - data = request.POST.dict() - - # Validate input data - location = data.get('location', '').strip() - report_type = data.get('report_type', 'current') - - # Location validation - if not location: - return JsonResponse({'error': 'Location is required'}, status=400) - - if len(location) > 100: - return JsonResponse({'error': 'Location name too long (max 100 characters)'}, status=400) - - # Report type validation - valid_reports = ['current', 'detailed', 'forecast'] - if report_type not in valid_reports: - return JsonResponse({'error': f'Invalid report type. Must be one of: {", ".join(valid_reports)}'}, status=400) - - # Get agent - agent = BaseAgent.objects.get(slug='weather-reporter') - - # Check wallet balance - if not request.user.has_sufficient_balance(agent.price): - return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - - # Create request object (no wallet deduction yet - only after successful processing) - agent_request = WeatherReporterRequest.objects.create( - user=request.user, - agent=agent, - cost=agent.price, - location=location, - report_type=report_type - ) - - # Process request immediately (API-based agent) - processor = WeatherReporterProcessor() - result = processor.process_request( - request_obj=agent_request, - user_id=request.user.id, - location=location, - report_type=report_type, - ) - - # Refresh user from database to get updated wallet balance - request.user.refresh_from_db() - - # Check if we have a response object - if hasattr(agent_request, 'response'): - response_obj = agent_request.response - return JsonResponse({ - 'success': response_obj.success, - 'status': 'completed', - 'content': response_obj.formatted_report, - 'weather_data': response_obj.weather_data, - 'temperature': response_obj.temperature, - 'description': response_obj.description, - 'humidity': response_obj.humidity, - 'wind_speed': response_obj.wind_speed, - 'formatted_report': response_obj.formatted_report, - 'processing_time': float(response_obj.processing_time) if response_obj.processing_time else None, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - # Fallback if no response object - return JsonResponse({ - 'success': True, - 'status': 'completed', - 'message': 'Weather report generated successfully', - 'wallet_balance': float(request.user.wallet_balance) - }) - - except BaseAgent.DoesNotExist: - return JsonResponse({'error': 'Weather Reporter agent not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -@login_required -def weather_reporter_result(request, request_id): - """Get result for a specific request""" - try: - agent_request = WeatherReporterRequest.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': response.success, - 'status': agent_request.status, - 'weather_data': getattr(response, 'weather_data', None), - 'temperature': getattr(response, 'temperature', None), - 'description': getattr(response, 'description', None), - 'humidity': getattr(response, 'humidity', None), - 'wind_speed': getattr(response, 'wind_speed', None), - 'formatted_report': getattr(response, 'formatted_report', None), - - 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except WeatherReporterRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -@login_required -def weather_reporter_status(request, request_id): - """Get status for a specific request (for polling)""" - try: - agent_request = WeatherReporterRequest.objects.get( - id=request_id, - user=request.user - ) - - if hasattr(agent_request, 'response'): - response = agent_request.response - # Refresh user to get current wallet balance - request.user.refresh_from_db() - - return JsonResponse({ - 'success': response.success, - 'status': agent_request.status, - 'content': getattr(response, 'weather_data', None), - 'weather_data': getattr(response, 'weather_data', None), - 'temperature': getattr(response, 'temperature', None), - 'description': getattr(response, 'description', None), - 'humidity': getattr(response, 'humidity', None), - 'wind_speed': getattr(response, 'wind_speed', None), - 'formatted_report': getattr(response, 'formatted_report', None), - 'raw_response': getattr(response, 'raw_response', None), - 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message, - 'wallet_balance': float(request.user.wallet_balance) - }) - else: - return JsonResponse({ - 'success': False, - 'status': agent_request.status, - 'message': 'Processing in progress...' - }) - - except WeatherReporterRequest.DoesNotExist: - return JsonResponse({'error': 'Request not found'}, status=404) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/workflows/templates/workflows/components/quick_agents_panel.html b/workflows/templates/workflows/components/quick_agents_panel.html index 3a040a9..3c7b4da 100644 --- a/workflows/templates/workflows/components/quick_agents_panel.html +++ b/workflows/templates/workflows/components/quick_agents_panel.html @@ -62,6 +62,6 @@ \ No newline at end of file diff --git a/workflows/templates/workflows/marketplace.html b/workflows/templates/workflows/marketplace.html new file mode 100644 index 0000000..80c0541 --- /dev/null +++ b/workflows/templates/workflows/marketplace.html @@ -0,0 +1,352 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}AI Agent Marketplace - Quantum Tasks AI{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} +
+ +
+

šŸ¤– AI Agent Marketplace

+

+ Discover powerful AI agents to automate your tasks, boost productivity, and streamline your workflow +

+
+ + +
+ + +
+ + All + + {% for category in all_categories %} + + {{ category }} + + {% endfor %} +
+
+ + +
+ {% if search_query %} + Search results for "{{ search_query }}" • + {% endif %} + {% if selected_category %} + {{ selected_category|capfirst }} category • + {% endif %} + {{ total_agents }} agent{{ total_agents|pluralize }} available +
+ + + {% if agents_by_category %} + {% for category, agents in agents_by_category.items %} + + {% endfor %} + {% else %} +
+
šŸ”
+

No agents found

+

Try adjusting your search or browse all categories

+ Browse All Agents +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/workflows/urls.py b/workflows/urls.py index e678d99..0c4ad76 100644 --- a/workflows/urls.py +++ b/workflows/urls.py @@ -4,6 +4,9 @@ from . import views app_name = 'workflows' urlpatterns = [ + # Marketplace view at /agents/ + path('', views.marketplace_view, name='marketplace'), + # Universal agent handler - matches any agent slug re_path(r'^(?P[\w-]+)/$', views.workflow_handler, name='agent'), diff --git a/workflows/views.py b/workflows/views.py index fb63e55..edb4896 100644 --- a/workflows/views.py +++ b/workflows/views.py @@ -10,14 +10,61 @@ import time import requests from datetime import datetime -from agent_base.models import BaseAgent from .models import WorkflowRequest, WorkflowResponse, WorkflowAnalytics -from .config.agents import get_agent_config, format_message_for_n8n, get_available_agents +from .config.agents import get_agent_config, format_message_for_n8n, get_available_agents, get_all_agents import logging logger = logging.getLogger(__name__) +def marketplace_view(request): + """Agent marketplace using AGENT_CONFIGS - no database dependency""" + agents_data = get_all_agents() + + # Group agents by category + agents_by_category = {} + all_categories = set() + + for agent_slug, agent_config in agents_data.items(): + category = agent_config.get('category', 'utilities') + all_categories.add(category) + + if category not in agents_by_category: + agents_by_category[category] = [] + + # Add slug to agent data for URL generation + agent_data = agent_config.copy() + agent_data['slug'] = agent_slug + agents_by_category[category].append(agent_data) + + # Filter by category if specified + selected_category = request.GET.get('category') + if selected_category and selected_category in all_categories: + agents_by_category = {selected_category: agents_by_category[selected_category]} + + # Search functionality + search_query = request.GET.get('search', '').strip() + if search_query: + filtered_agents = {} + for category, agents in agents_by_category.items(): + filtered_agents[category] = [ + agent for agent in agents + if search_query.lower() in agent['name'].lower() or + search_query.lower() in agent['description'].lower() + ] + agents_by_category = {k: v for k, v in filtered_agents.items() if v} + + context = { + 'agents_by_category': agents_by_category, + 'all_categories': sorted(all_categories), + 'selected_category': selected_category, + 'search_query': search_query, + 'total_agents': len(agents_data) + } + + return render(request, 'workflows/marketplace.html', context) + + def send_file_to_webhook(webhook_url, uploaded_file, form_data, timeout=60): """Send file to N8N webhook endpoint""" logger.info(f"šŸ” DEBUG: send_file_to_webhook called!") @@ -104,11 +151,14 @@ def workflow_handler(request, agent_slug): if not agent_config: raise Http404("Agent configuration not found") - # Get agent from BaseAgent model - try: - agent = BaseAgent.objects.get(slug=agent_slug, is_active=True) - except BaseAgent.DoesNotExist: - raise Http404("Agent not found") + # Agent config serves as the agent data (no database dependency) + agent = { + 'slug': agent_slug, + 'name': agent_config['name'], + 'price': agent_config['price'], + 'icon': agent_config['icon'], + 'description': agent_config['description'] + } if request.method == 'POST': return process_workflow_request(request, agent_slug, agent_config, agent)