diff --git a/job_posting_generator/__init__.py b/job_posting_generator/__init__.py new file mode 100644 index 0000000..3b51fe4 --- /dev/null +++ b/job_posting_generator/__init__.py @@ -0,0 +1 @@ +# 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 new file mode 100644 index 0000000..9185711 --- /dev/null +++ b/job_posting_generator/admin.py @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..b3514ef --- /dev/null +++ b/job_posting_generator/apps.py @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..6321888 --- /dev/null +++ b/job_posting_generator/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# 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 new file mode 100644 index 0000000..e69de29 diff --git a/job_posting_generator/models.py b/job_posting_generator/models.py new file mode 100644 index 0000000..a013bfb --- /dev/null +++ b/job_posting_generator/models.py @@ -0,0 +1,75 @@ +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/processor.py b/job_posting_generator/processor.py new file mode 100644 index 0000000..0cf062c --- /dev/null +++ b/job_posting_generator/processor.py @@ -0,0 +1,105 @@ +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 new file mode 100644 index 0000000..b2fc5c4 --- /dev/null +++ b/job_posting_generator/templates/job_posting_generator/detail.html @@ -0,0 +1,850 @@ +{% load static %} + + + + + + Job Posting Generator Agent - NetCop AI Hub + + + +
+ + + +
+ +
+

+ 💼 Job Posting Generator Agent +

+

+ Create professional job postings with detailed requirements, company culture integration, and SEO optimization. +

+
+ 💰 Cost: 4.00 AED +
+
+ + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + + +
+ +
+
+ {% csrf_token %} + + +
+

📝 Basic Information

+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ Include key responsibilities, day-to-day tasks, and what success looks like in this role. +
+
+
+ + +
+

🎯 Position Details

+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+

🏢 Company Information

+ +
+ + +
+ Optional: Include company website for better context and SEO. +
+
+ +
+ + +
+ Optional: Specific application instructions, required documents, or contact information. +
+
+
+ + +
+
+ ⏳ Processing... +
+
+ Creating your professional job posting... +
+
+
+
+ + +
+ +
+

💳 Your Wallet

+ +
+
+ {% if user.is_authenticated %} + {{ user.wallet_balance|floatformat:2 }} AED + {% else %} + 0.00 AED + {% endif %} +
+
+ Available Balance +
+
+ + {% if user.is_authenticated %} + {% if user.wallet_balance >= 4.00 %} + + {% else %} +
+ Insufficient balance! You need 4.00 AED. +
+ + 💰 Top Up Wallet + + {% endif %} + {% else %} + + 🔑 Login to Continue + + {% endif %} +
+ + +
+

+ 💡 How it works +

+
    +
  • + + Enter job and company details +
  • +
  • + + AI generates professional posting +
  • +
  • + + Includes requirements & culture +
  • +
  • + + SEO optimized for job boards +
  • +
+
+
+
+ + +
+ +
+
+

+ Professional Job Posting +

+
+ ✅ Complete +
+
+ + +
+ +
+ + +
+ + + + + +
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/job_posting_generator/urls.py b/job_posting_generator/urls.py new file mode 100644 index 0000000..8087a1d --- /dev/null +++ b/job_posting_generator/urls.py @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..a5f1074 --- /dev/null +++ b/job_posting_generator/views.py @@ -0,0 +1,172 @@ +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 741544b..ef64ea0 100644 --- a/netcop_hub/settings.py +++ b/netcop_hub/settings.py @@ -49,6 +49,8 @@ INSTALLED_APPS = [ 'agent_base', 'weather_reporter', 'data_analyzer', + 'job_posting_generator', + 'social_ads_generator', ] MIDDLEWARE = [ diff --git a/netcop_hub/urls.py b/netcop_hub/urls.py index feabd57..1d7a776 100644 --- a/netcop_hub/urls.py +++ b/netcop_hub/urls.py @@ -24,6 +24,8 @@ urlpatterns = [ path('auth/', include('authentication.urls')), path('agents/weather-reporter/', include('weather_reporter.urls')), path('agents/data-analyzer/', include('data_analyzer.urls')), + path('agents/job-posting-generator/', include('job_posting_generator.urls')), + path('agents/social-ads-generator/', include('social_ads_generator.urls')), path('', include('core.urls')), ] diff --git a/social_ads_generator/__init__.py b/social_ads_generator/__init__.py new file mode 100644 index 0000000..e0610d4 --- /dev/null +++ b/social_ads_generator/__init__.py @@ -0,0 +1 @@ +# 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 new file mode 100644 index 0000000..3526be7 --- /dev/null +++ b/social_ads_generator/admin.py @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..b929035 --- /dev/null +++ b/social_ads_generator/apps.py @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..c819990 --- /dev/null +++ b/social_ads_generator/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# 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 new file mode 100644 index 0000000..e69de29 diff --git a/social_ads_generator/models.py b/social_ads_generator/models.py new file mode 100644 index 0000000..0cce58b --- /dev/null +++ b/social_ads_generator/models.py @@ -0,0 +1,66 @@ +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/processor.py b/social_ads_generator/processor.py new file mode 100644 index 0000000..bb24ca7 --- /dev/null +++ b/social_ads_generator/processor.py @@ -0,0 +1,121 @@ +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" + + # Build comprehensive social ads prompt + prompt = f""" +Create a compelling social media advertisement for the following: + +Product/Service Description: +{request_obj.description} + +Target Platform: {request_obj.get_social_platform_display()} +Language: {request_obj.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 {request_obj.get_social_platform_display()} audience +- Uses {request_obj.language} language +""" + + 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 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 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 + + # 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 = bool(ad_copy.strip()) and len(ad_copy.strip()) > 20 + + # 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 + 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}") \ No newline at end of file diff --git a/social_ads_generator/templates/social_ads_generator/detail.html b/social_ads_generator/templates/social_ads_generator/detail.html new file mode 100644 index 0000000..03e29ad --- /dev/null +++ b/social_ads_generator/templates/social_ads_generator/detail.html @@ -0,0 +1,941 @@ +{% load static %} + + + + + + Social Ads Generator Agent - NetCop AI Hub + + + +
+ + + +
+ +
+

+ 📱 Social Ads Generator Agent +

+

+ Create compelling social media advertisements with platform-optimized copy, audience targeting suggestions, and A/B test variants. +

+
+ 💰 Cost: 7.00 AED +
+
+ + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + + +
+ +
+
+ {% csrf_token %} + + +
+

📝 Describe Your Product/Service

+ +
+ + +
+ Be specific about features, benefits, and target audience for better results. +
+
+
+ + +
+

📱 Target Platform

+ +
+ + + + + + + + + + + +
+
+ + +
+

⚙️ Additional Options

+ + +
+ +
+ Emojis can increase engagement but may not be suitable for all brands. +
+
+ + +
+ + +
+
+ + +
+
+ ⏳ Processing... +
+
+ Creating your social media ad... +
+
+
+
+ + +
+ +
+

💳 Your Wallet

+ +
+
+ {% if user.is_authenticated %} + {{ user.wallet_balance|floatformat:2 }} AED + {% else %} + 0.00 AED + {% endif %} +
+
+ Available Balance +
+
+ + {% if user.is_authenticated %} + {% if user.wallet_balance >= 7.00 %} + + {% else %} +
+ Insufficient balance! You need 7.00 AED. +
+ + 💰 Top Up Wallet + + {% endif %} + {% else %} + + 🔑 Login to Continue + + {% endif %} +
+ + +
+

+ 💡 How it works +

+
    +
  • + + Describe your product/service +
  • +
  • + + Choose target platform +
  • +
  • + + Get optimized ad copy +
  • +
  • + + Includes A/B test variants +
  • +
+
+
+
+ + +
+ +
+
+

+ Social Media Ad Copy +

+
+ ✅ Complete +
+
+ + +
+

+ 📱 Optimized for Platform +

+
+ + +
+

+ 📝 Generated Ad Copy +

+ +
+ +
+ +
+
+ #️⃣ Suggested Hashtags +
+
+ +
+
+ +
+
+ Generated: Just now +
+
+
+ + +
+ + + + + +
+
+
+ + +
+
+

+ © 2024 NetCop AI Hub. Powered by AI agents. +

+
+
+
+ + + + \ No newline at end of file diff --git a/social_ads_generator/urls.py b/social_ads_generator/urls.py new file mode 100644 index 0000000..a272331 --- /dev/null +++ b/social_ads_generator/urls.py @@ -0,0 +1,9 @@ +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_result, name='status'), +] \ No newline at end of file diff --git a/social_ads_generator/views.py b/social_ads_generator/views.py new file mode 100644 index 0000000..de39b7c --- /dev/null +++ b/social_ads_generator/views.py @@ -0,0 +1,164 @@ +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 SocialAdsGeneratorRequest, SocialAdsGeneratorResponse +from .processor import SocialAdsGeneratorProcessor +import json + + +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: + # Create request object (no wallet deduction yet) + agent_request = SocialAdsGeneratorRequest.objects.create( + user=request.user, + agent=agent, + cost=agent.price, + description=request.POST.get('description'), + social_platform=request.POST.get('social_platform', 'facebook'), + include_emoji=request.POST.get('include_emoji') == 'on', + language=request.POST.get('language', 'English'), + ) + + # 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: + return JsonResponse({'error': str(e)}, 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) + + +@method_decorator(csrf_exempt, name='dispatch') +class SocialAdsGeneratorProcessView(View): + """Process Social Ads 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='social-ads-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 = SocialAdsGeneratorRequest.objects.create( + user=request.user, + agent=agent, + cost=agent.price, + description=data.get('description'), + social_platform=data.get('social_platform', 'facebook'), + include_emoji=data.get('include_emoji', False), + language=data.get('language', 'English'), + ) + + # 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 Generator request processed successfully', + 'wallet_balance': float(request.user.wallet_balance) + }) + + except BaseAgent.DoesNotExist: + return JsonResponse({'error': 'Social Ads Generator agent not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@login_required +def social_ads_generator_result(request, request_id): + """Get result for a specific request""" + 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() + + return JsonResponse({ + 'success': response.success, + 'status': agent_request.status, + 'content': getattr(response, 'ad_copy', None), + 'ad_copy': getattr(response, 'ad_copy', None), + 'hashtags': getattr(response, 'hashtags', None), + 'targeting_suggestions': getattr(response, 'targeting_suggestions', None), + 'formatted_ad': getattr(response, 'formatted_ad', 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 SocialAdsGeneratorRequest.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