🗑️ Complete agent_base app removal and legacy cleanup

- Removed entire agent_base app and all legacy individual agent apps
- Updated core views to use workflows config instead of database models
- Fixed all template references to use workflows:marketplace
- Removed agent_base logging configuration from settings
- Created unified marketplace in workflows app using AGENT_CONFIGS
- All 6 agents now working through unified workflows system
- Direct N8N integration without Django fallbacks
- Simplified architecture with single source of truth for agent metadata

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2025-07-29 19:57:24 +05:30
parent 73d514152c
commit ec42b51d7f
117 changed files with 451 additions and 13323 deletions

View File

@ -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

View File

@ -1 +0,0 @@
# Agent Base Framework

View File

@ -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

View File

@ -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'

View File

@ -1 +0,0 @@
# {{ agent_name }} Agent App

View File

@ -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']

View File

@ -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'

View File

@ -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

View File

@ -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 }}'

View File

@ -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/<uuid:request_id>/', views.{{ agent_slug_underscore }}_result, name='result'),
]

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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}")

View File

@ -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"
)
)

View File

@ -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!")

View File

@ -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)

View File

@ -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/")

View File

@ -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}")

View File

@ -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"
)
)

View File

@ -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"))

View File

@ -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")

View File

@ -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'],
},
),
]

View File

@ -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

View File

@ -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'

View File

@ -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'),
]

View File

@ -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,
})

View File

@ -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]
}

View File

@ -1 +0,0 @@
# Data Analysis Agent Agent App

View File

@ -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']

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class DataAnalysisAgentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'data_analyzer'

View File

@ -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")
)

View File

@ -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',
},
),
]

View File

@ -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
]

View File

@ -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),
),
]

View File

@ -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
]

View File

@ -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/",
),
),
]

View File

@ -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}")

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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}")

View File

@ -1,929 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Data Analyzer - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
{% endblock %}
{% block content %}
<script>
// Consolidated DOMContentLoaded initialization
document.addEventListener('DOMContentLoaded', function() {
// Set data for JavaScript access
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-agent-price', '{{ agent.price }}');
// Initialize form submission
const form = document.getElementById('agentForm');
if (form) {
form.addEventListener('submit', handleFormSubmission);
}
// Initialize file upload
const fileInput = document.getElementById('dataFile');
if (fileInput) {
fileInput.addEventListener('change', handleFileChange);
}
// Initialize drag and drop
const uploadArea = document.querySelector('.file-upload-area');
if (uploadArea) {
setupDragAndDrop(uploadArea, fileInput);
}
// Set initial radio selection
const firstRadio = document.querySelector('.radio-card');
if (firstRadio && !document.querySelector('.radio-card.selected')) {
firstRadio.classList.add('selected');
const input = firstRadio.querySelector('input[type="radio"]');
if (input) input.checked = true;
}
});
// Data Analyzer Utils
const DataAnalyzerUtils = {
// Update wallet balance display
updateWalletBalance(newBalance) {
if (newBalance !== undefined) {
// Update header balance
const headerBalance = document.querySelector('a[data-wallet-balance]');
if (headerBalance) {
headerBalance.textContent = `💰 ${newBalance.toFixed(2)} AED`;
}
// Update page balance
const pageBalance = document.getElementById('walletBalance');
if (pageBalance) {
pageBalance.textContent = newBalance.toFixed(2);
}
// Update all data attributes
document.querySelectorAll('[data-wallet-balance]').forEach(element => {
element.textContent = `${newBalance.toFixed(2)} AED`;
});
// Store current balance globally
window.currentWalletBalance = newBalance;
}
},
// Show toast notification
showToast(message, type = 'info') {
// Remove existing toasts
document.querySelectorAll('.toast').forEach(toast => toast.remove());
// Create new toast
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
// Add to page
document.body.appendChild(toast);
// Show toast
setTimeout(() => toast.classList.add('show'), 100);
// Auto remove after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
},
// Display processing status
showProcessing() {
const processingStatus = document.getElementById('processingStatus');
const resultsContainer = document.getElementById('resultsContainer');
if (processingStatus) processingStatus.style.display = 'block';
if (resultsContainer) resultsContainer.style.display = 'none';
this.showToast('🔄 Processing your data file...', 'success');
},
// Hide processing status
hideProcessing() {
const processingStatus = document.getElementById('processingStatus');
if (processingStatus) processingStatus.style.display = 'none';
},
// Display analysis results
displayResults(result) {
const resultsContainer = document.getElementById('resultsContainer');
const resultsContent = document.getElementById('resultsContent');
const processingStatus = document.getElementById('processingStatus');
if (result.success) {
// Hide processing status
this.hideProcessing();
// Show results with rich formatting
const analysisText = result.report_text || result.insights_summary || result.analysis_results || 'Analysis completed successfully.';
if (resultsContent) {
// Convert newlines to HTML and preserve formatting
const formattedText = analysisText
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/### (.*?)(<br>|$)/g, '<h3>$1</h3>')
.replace(/## (.*?)(<br>|$)/g, '<h2>$1</h2>')
.replace(/# (.*?)(<br>|$)/g, '<h1>$1</h1>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>');
resultsContent.innerHTML = `<p>${formattedText}</p>`;
}
// Show results container
if (resultsContainer) {
resultsContainer.style.display = 'block';
}
// Update wallet balance if provided
if (result.wallet_balance !== undefined) {
this.updateWalletBalance(result.wallet_balance);
}
this.showToast('✅ Data analysis completed successfully!', 'success');
} else if (result.error) {
this.hideProcessing();
this.showToast(`❌ Error: ${result.error}`, 'error');
} else {
this.hideProcessing();
this.showToast('❌ Analysis failed. Please try again.', 'error');
}
}
};
// For backward compatibility
const AgentUtils = DataAnalyzerUtils;
// Modern Radio Selection
function selectRadio(value) {
// Remove selected class from all cards
document.querySelectorAll('.radio-card').forEach(card => {
card.classList.remove('selected');
});
// Add selected class to clicked card
const selectedCard = document.querySelector(`input[value="${value}"]`).closest('.radio-card');
if (selectedCard) {
selectedCard.classList.add('selected');
}
// Select the radio button
const radioInput = document.getElementById(value);
if (radioInput) {
radioInput.checked = true;
}
}
// File upload handling
function handleFileChange(event) {
const file = event.target.files[0];
const uploadArea = document.querySelector('.file-upload-area');
const uploadText = document.querySelector('.upload-text');
if (file) {
uploadArea.classList.add('file-selected');
uploadText.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span>📄</span>
<div>
<div style="font-weight: 500;">${file.name}</div>
<div style="font-size: 12px; color: var(--on-surface-variant);">${(file.size / 1024 / 1024).toFixed(2)} MB</div>
</div>
</div>
`;
DataAnalyzerUtils.showToast(`File selected: ${file.name}`, 'success');
} else {
uploadArea.classList.remove('file-selected');
uploadText.innerHTML = `
<div class="upload-icon">📁</div>
<div><strong>Click to upload</strong> or drag and drop</div>
<div>PDF files only</div>
`;
}
}
// Drag and drop setup
function setupDragAndDrop(uploadArea, fileInput) {
let dragCounter = 0;
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
uploadArea.classList.add('drag-over');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
dragCounter--;
if (dragCounter <= 0) {
uploadArea.classList.remove('drag-over');
dragCounter = 0;
}
}
function handleDragEnter(e) {
e.preventDefault();
e.stopPropagation();
dragCounter++;
uploadArea.classList.add('drag-over');
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
dragCounter = 0;
uploadArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
handleFileChange({ target: fileInput });
}
}
uploadArea.addEventListener('dragenter', handleDragEnter);
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
}
// Form validation
function isFormValid() {
const fileInput = document.getElementById('dataFile');
const analysisType = document.querySelector('input[name="analysisType"]:checked');
if (!fileInput.files || fileInput.files.length === 0) {
DataAnalyzerUtils.showToast('Please select a data file', 'error');
return false;
}
if (!analysisType) {
DataAnalyzerUtils.showToast('Please select an analysis type', 'error');
return false;
}
return true;
}
// Copy analysis results
function copyResults() {
const content = document.getElementById('resultsContent');
if (content) {
const text = content.textContent || '';
navigator.clipboard.writeText(text).then(() => {
DataAnalyzerUtils.showToast('📋 Copied to clipboard!', 'success');
}).catch(() => {
DataAnalyzerUtils.showToast('❌ Failed to copy to clipboard', 'error');
});
}
}
// Download analysis results
function downloadResults() {
const content = document.getElementById('resultsContent');
if (content) {
const text = content.textContent || '';
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'data-analysis-results.txt';
a.click();
URL.revokeObjectURL(url);
// No toast for download - file download is confirmation enough
}
}
// Reset form for new analysis
function resetForm() {
const form = document.getElementById('agentForm');
if (form) {
form.reset();
}
const resultsContainer = document.getElementById('resultsContainer');
const processingStatus = document.getElementById('processingStatus');
if (resultsContainer) resultsContainer.style.display = 'none';
if (processingStatus) processingStatus.style.display = 'none';
// Reset file upload area
const uploadArea = document.querySelector('.file-upload-area');
const uploadText = document.querySelector('.upload-text');
if (uploadArea) uploadArea.classList.remove('file-selected');
if (uploadText) {
uploadText.innerHTML = `
<div class="upload-icon">📁</div>
<div><strong>Click to upload</strong> or drag and drop</div>
<div>PDF files only</div>
`;
}
// Reset radio selection
document.querySelectorAll('.radio-card').forEach(card => {
card.classList.remove('selected');
});
const firstCard = document.querySelector('.radio-card');
if (firstCard) {
firstCard.classList.add('selected');
const input = firstCard.querySelector('input[type="radio"]');
if (input) input.checked = true;
}
// No toast for reset - visual feedback is enough
}
// Quick Agent Access Functions
function toggleQuickAgents() {
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
const toggle = document.querySelector('.quick-agent-toggle');
if (!panel || !overlay) return;
const isActive = panel.classList.contains('active');
if (isActive) {
// Close panel
panel.classList.remove('active');
overlay.classList.remove('active');
if (toggle) toggle.classList.remove('active');
// Update ARIA attributes
if (toggle) toggle.setAttribute('aria-expanded', 'false');
panel.setAttribute('aria-hidden', 'true');
overlay.setAttribute('aria-hidden', 'true');
} else {
// Open panel
panel.classList.add('active');
overlay.classList.add('active');
if (toggle) toggle.classList.add('active');
// Update ARIA attributes
if (toggle) toggle.setAttribute('aria-expanded', 'true');
panel.setAttribute('aria-hidden', 'false');
overlay.setAttribute('aria-hidden', 'false');
}
}
function closeQuickAgents() {
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
const toggle = document.querySelector('.quick-agent-toggle');
if (panel) panel.classList.remove('active');
if (overlay) overlay.classList.remove('active');
if (toggle) toggle.classList.remove('active');
// Update ARIA attributes
if (toggle) toggle.setAttribute('aria-expanded', 'false');
if (panel) panel.setAttribute('aria-hidden', 'true');
if (overlay) overlay.setAttribute('aria-hidden', 'true');
}
// Form submission handler
function handleFormSubmission(e) {
e.preventDefault();
if (!isFormValid()) {
return;
}
// Check authentication and balance
const isAuthenticated = document.body.getAttribute('data-user-authenticated') === 'true';
if (!isAuthenticated) {
DataAnalyzerUtils.showToast('Please login to continue', 'error');
window.location.href = "{% url 'authentication:login' %}";
return;
}
const walletBalance = parseFloat(document.getElementById('walletBalance')?.textContent) || 0;
if (walletBalance < {{ agent.price }}) {
DataAnalyzerUtils.showToast('Insufficient wallet balance', 'error');
setTimeout(() => {
window.location.href = "{% url 'wallet:wallet' %}";
}, 2000);
return;
}
// Show processing status
DataAnalyzerUtils.showProcessing();
// Submit form with AJAX
const formData = new FormData(e.target);
fetch(window.location.href, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.success && result.request_id) {
// Start polling for results
checkResults(result.request_id);
DataAnalyzerUtils.updateWalletBalance(result.wallet_balance);
} else {
DataAnalyzerUtils.hideProcessing();
DataAnalyzerUtils.showToast(`❌ ${result.error || 'Processing failed'}`, 'error');
}
})
.catch(error => {
console.error('Form submission error:', error);
DataAnalyzerUtils.hideProcessing();
DataAnalyzerUtils.showToast('❌ Connection error. Please try again.', 'error');
});
}
// Check results (polling for webhook completion)
function checkResults(requestId) {
let pollCount = 0;
const maxPolls = 30; // 5 minutes max
const pollInterval = setInterval(() => {
pollCount++;
fetch(`/agents/data-analyzer/status/${requestId}/`)
.then(response => response.json())
.then(result => {
if (result.status === 'completed') {
clearInterval(pollInterval);
DataAnalyzerUtils.displayResults(result);
} else if (result.status === 'failed') {
clearInterval(pollInterval);
DataAnalyzerUtils.hideProcessing();
DataAnalyzerUtils.showToast('❌ Analysis failed. Please try again.', 'error');
} else if (pollCount >= maxPolls) {
clearInterval(pollInterval);
DataAnalyzerUtils.hideProcessing();
DataAnalyzerUtils.showToast('⏰ Analysis is taking longer than expected. Please check back later.', 'error');
}
// Continue polling if still processing
})
.catch(error => {
console.error('Status check error:', error);
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
DataAnalyzerUtils.hideProcessing();
DataAnalyzerUtils.showToast('❌ Connection error during processing.', 'error');
}
});
}, 10000); // Check every 10 seconds
}
// Close panel with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeQuickAgents();
}
});
</script>
<div class="agent-container">
<!-- Agent Header -->
{% include "components/agent_header.html" with agent_title="Data Analyzer" agent_subtitle="AI-powered analysis of your data files with comprehensive insights" %}
<!-- Quick Agent Access Panel -->
{% include "components/quick_agents_panel.html" %}
<!-- Agent Grid -->
<div class="agent-grid">
<!-- Data Analysis Form Widget -->
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">📊</span>
Data Analysis Configuration
</h3>
</div>
<div class="widget-content">
<form id="agentForm" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<!-- File Upload -->
<div class="form-group">
<label class="form-label">📁 Upload Data File</label>
<div class="file-upload-area" onclick="document.getElementById('dataFile').click()"
role="button" tabindex="0" aria-label="Click to upload data file or drag and drop"
onkeydown="if(event.key==='Enter'||event.key===' '){document.getElementById('dataFile').click()}">
<div class="upload-text">
<div class="upload-icon">📁</div>
<div><strong>Click to upload</strong> or drag and drop</div>
<div>PDF files only</div>
</div>
</div>
<input type="file" id="dataFile" name="file" accept=".pdf" style="display: none;" required>
<div class="form-help">Supported format: PDF files only. Max size: 10MB</div>
</div>
<!-- Analysis Type Selection -->
<div class="form-group">
<label class="form-label">📈 Analysis Type</label>
<div class="radio-grid">
<div class="radio-card selected" onclick="selectRadio('summary')">
<input type="radio" id="summary" name="analysisType" value="summary" checked>
<div class="radio-button"></div>
<label for="summary" class="radio-label">📋 Summary</label>
</div>
<div class="radio-card" onclick="selectRadio('detailed')">
<input type="radio" id="detailed" name="analysisType" value="detailed">
<div class="radio-button"></div>
<label for="detailed" class="radio-label">📈 Detailed</label>
</div>
<div class="radio-card" onclick="selectRadio('statistical')">
<input type="radio" id="statistical" name="analysisType" value="statistical">
<div class="radio-button"></div>
<label for="statistical" class="radio-label">🔢 Statistical</label>
</div>
</div>
</div>
<!-- Submit Button -->
<div style="margin-top: var(--spacing-lg);">
{% if user.is_authenticated %}
{% if user.wallet_balance >= agent.price %}
<button type="submit" class="btn btn-primary btn-full" id="analyzeBtn">
🚀 Analyze Data ({{ agent.price }} AED)
</button>
{% else %}
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
Insufficient balance! You need {{ agent.price }} AED.
</div>
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
🔐 Login to Continue
</a>
{% endif %}
</div>
</form>
</div>
</div>
<!-- How It Works Widget - Positioned on the right -->
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon"></span>
How It Works
</h3>
</div>
<div class="widget-content">
<ol class="info-list">
<li>Upload your PDF file</li>
<li>Choose analysis type and preferences</li>
<li>Our AI analyzes your data</li>
<li>Get comprehensive insights and reports</li>
</ol>
<!-- Other Agents Button -->
<button class="quick-agent-toggle btn btn-secondary btn-full" onclick="toggleQuickAgents()"
title="Quick access to other agents"
aria-label="Open quick access panel for other AI agents"
aria-expanded="false"
aria-controls="quickAgentsPanel"
style="margin-top: var(--spacing-md);">
<span class="toggle-icon" aria-hidden="true">🚀</span>
<span class="toggle-text">Explore Other Agents</span>
</button>
</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="agent-grid">
<!-- Processing Status -->
{% include "components/processing_status.html" with status_title="Analyzing Your Data..." status_text="Please wait while our AI processes your file..." %}
<!-- Results Widget -->
{% include "components/results_container.html" with results_title="Analysis Results" %}
</div>
</div>
<style>
/* Data Analyzer Specific Styles */
.file-upload-area {
border: 2px dashed var(--outline);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
background: var(--surface-variant);
margin-bottom: var(--spacing-sm);
}
.file-upload-area:hover {
border-color: var(--primary);
background: var(--surface);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.file-upload-area.drag-over {
border-color: var(--primary);
background: rgba(0, 0, 0, 0.02);
transform: scale(1.02);
}
.file-upload-area.file-selected {
border-color: var(--success);
background: #f0fdf4;
color: #16a34a;
}
.upload-icon {
font-size: 48px;
margin-bottom: var(--spacing-md);
opacity: 0.7;
}
.upload-text {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
color: var(--on-surface-variant);
}
.upload-text > div:first-child {
font-weight: 500;
color: var(--on-surface);
}
.radio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
}
.radio-card {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
background: var(--surface);
position: relative;
}
.radio-card:hover {
border-color: var(--primary);
background: var(--surface-variant);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.radio-card.selected {
border-color: var(--primary);
background: rgba(0, 0, 0, 0.02);
}
.radio-card input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.radio-button {
width: 20px;
height: 20px;
border: 2px solid var(--outline);
border-radius: 50%;
position: relative;
transition: all 0.2s ease;
flex-shrink: 0;
}
.radio-card.selected .radio-button {
border-color: var(--primary);
}
.radio-card.selected .radio-button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
background: var(--primary);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.radio-label {
font-size: 14px;
font-weight: 500;
color: var(--on-surface);
cursor: pointer;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Toast Notifications */
.toast {
position: fixed;
top: 20px;
right: 20px;
background: var(--surface);
border: 1px solid var(--outline);
border-radius: var(--radius-md);
padding: var(--spacing-md) var(--spacing-lg);
box-shadow: var(--shadow-lg);
z-index: 1000;
max-width: 400px;
font-size: 14px;
font-weight: 500;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.toast.show {
transform: translateX(0);
}
.toast.success {
border-color: var(--success);
background: #f0fdf4;
color: #16a34a;
}
.toast.error {
border-color: var(--error);
background: #fef2f2;
color: #dc2626;
}
/* Enhanced Results Display - Restored from Original */
.results-content {
background: var(--surface-variant);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
line-height: 1.7;
color: var(--on-surface);
font-size: 15px;
}
/* Enhanced Results Typography */
.results-content h1,
.results-content h2,
.results-content h3 {
color: var(--primary);
font-weight: 700;
margin: var(--spacing-xl) 0 var(--spacing-md) 0;
line-height: 1.3;
}
.results-content h1 {
font-size: 24px;
border-bottom: 3px solid var(--primary);
padding-bottom: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.results-content h2 {
font-size: 20px;
margin-top: var(--spacing-xl);
position: relative;
padding-left: var(--spacing-md);
}
.results-content h2::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--primary);
border-radius: 2px;
}
.results-content h3 {
font-size: 18px;
color: var(--on-surface);
font-weight: 600;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-sm);
border-left: 4px solid var(--primary);
margin: var(--spacing-lg) 0 var(--spacing-md) 0;
}
.results-content p {
margin: var(--spacing-md) 0;
text-align: justify;
}
.results-content ul,
.results-content ol {
margin: var(--spacing-md) 0;
padding-left: var(--spacing-xl);
}
.results-content li {
margin: var(--spacing-sm) 0;
position: relative;
}
.results-content ul li::marker {
color: var(--primary);
font-weight: bold;
}
.results-content ol li::marker {
color: var(--primary);
font-weight: bold;
}
/* Modern Info Boxes */
.results-content .key-points,
.results-content .insights,
.results-content .summary {
background: var(--surface);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-md);
padding: var(--spacing-lg);
margin: var(--spacing-lg) 0;
box-shadow: var(--shadow-sm);
}
.results-content .key-points {
border-left: 4px solid #3b82f6;
}
.results-content .insights {
border-left: 4px solid #10b981;
}
.results-content .summary {
border-left: 4px solid #f59e0b;
}
/* Strong text styling */
.results-content strong {
color: var(--primary);
font-weight: 600;
}
/* Code and technical terms */
.results-content code {
background: #f1f5f9;
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 14px;
color: #1e293b;
}
@media (max-width: 768px) {
.radio-grid {
grid-template-columns: 1fr;
}
.toast {
left: 20px;
right: 20px;
max-width: none;
transform: translateY(-100%);
}
.toast.show {
transform: translateY(0);
}
.results-content {
padding: var(--spacing-md);
font-size: 14px;
}
.results-content h1 {
font-size: 20px;
}
.results-content h2 {
font-size: 18px;
}
.results-content h3 {
font-size: 16px;
padding: var(--spacing-sm) var(--spacing-md);
}
}
</style>
{% endblock %}

View File

@ -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/<uuid:request_id>/', views.data_analyzer_status, name='status'),
path('result/<uuid:request_id>/', views.data_analyzer_result, name='result'),
]

View File

@ -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)

View File

@ -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 ===

View File

@ -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

View File

@ -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"],
},
),
]

View File

@ -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}"

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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/<int:request_id>/', views.email_writer_status, name='status'),
]

View File

@ -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)

View File

@ -1 +0,0 @@
# 5 Whys Analysis Agent Agent App

View File

@ -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')
}),
)

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class FiveWhysAnalyzerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'five_whys_analyzer'

View File

@ -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'),
),
]

View File

@ -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'

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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')

View File

@ -1,793 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}5 Whys Analysis Agent - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
{% endblock %}
{% block content %}
<script>
// Agent Frontend Template - Common JavaScript Utilities
// Quick Agent Access Panel Functions
function toggleQuickAgents() {
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
const toggle = document.querySelector('.quick-agent-toggle');
const isActive = panel.classList.contains('active');
if (isActive) {
panel.classList.remove('active');
overlay.classList.remove('active');
toggle.classList.remove('active');
toggle.setAttribute('aria-expanded', 'false');
panel.setAttribute('aria-hidden', 'true');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = 'auto';
} else {
panel.classList.add('active');
overlay.classList.add('active');
toggle.classList.add('active');
toggle.setAttribute('aria-expanded', 'true');
panel.setAttribute('aria-hidden', 'false');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
}
function closeQuickAgents() {
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
const toggle = document.querySelector('.quick-agent-toggle');
if (panel && overlay && toggle) {
panel.classList.remove('active');
overlay.classList.remove('active');
toggle.classList.remove('active');
toggle.setAttribute('aria-expanded', 'false');
panel.setAttribute('aria-hidden', 'true');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = 'auto';
}
}
// Toast Notification Function
function showToast(message, type = 'info') {
document.querySelectorAll('.toast').forEach(toast => toast.remove());
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Processing Status Functions
function showProcessing() {
const processingStatus = document.getElementById('processingStatus');
processingStatus.style.display = 'block';
processingStatus.classList.add('active');
}
function hideProcessing() {
const processingStatus = document.getElementById('processingStatus');
processingStatus.style.display = 'none';
processingStatus.classList.remove('active');
}
// Wallet Balance Update Function
function updateWalletBalance(newBalance) {
if (newBalance !== undefined) {
const walletBalance = document.getElementById('walletBalance');
if (walletBalance) {
walletBalance.textContent = newBalance.toFixed(2);
}
}
}
// Copy to Clipboard Utility
function copyToClipboard(text, successMessage = 'Copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
showToast('📋 Copied to clipboard!', 'success');
}).catch(() => {
showToast('❌ Failed to copy to clipboard', 'error');
});
}
// Download as File Utility
function downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
// No toast for download - file download is confirmation enough
}
// Close panel on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeQuickAgents();
}
});
// Five Whys specific utilities
const FiveWhysUtils = {
copyReport() {
const content = document.getElementById('reportContent');
if (content) {
const text = content.textContent || content.innerText || '';
copyToClipboard(text);
}
},
downloadReport() {
const content = document.getElementById('reportContent');
if (content) {
const text = content.textContent || content.innerText || '';
downloadAsFile(text, `5-whys-report-${Date.now()}.txt`);
}
}
};
// Backward compatibility functions
function copyReport() { FiveWhysUtils.copyReport(); }
function downloadReport() { FiveWhysUtils.downloadReport(); }
</script>
<div class="agent-container">
<!-- Agent Header -->
{% include "components/agent_header.html" with agent_title="5 Whys Analyzer" agent_subtitle="AI-powered root cause analysis using the 5 Whys methodology" %}
<!-- Quick Agent Access Panel -->
{% include "components/quick_agents_panel.html" %}
<!-- Agent Grid -->
<div class="agent-grid">
<div class="agent-widget widget-large" style="flex: 1; margin-right: var(--spacing-lg);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">💬</span>
5 Whys Analysis Chat
</h3>
</div>
<div class="widget-content">
<!-- Chat Messages Container -->
<div id="chatContainer" class="chat-container">
<h4 class="section-subtitle">💬 Chat with 5 Whys Analyst</h4>
<!-- Welcome Message -->
<div class="welcome-message">
<div class="message-header">5 Whys Analyst</div>
<div class="message-content">
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?
</div>
</div>
<!-- Chat messages will be dynamically added here -->
<div id="chatMessages"></div>
</div>
<!-- Chat Input Form -->
<form id="chatForm" class="chat-form">
{% csrf_token %}
<div class="input-group">
<input
type="text"
id="chatInput"
name="message"
class="form-input"
placeholder="Ask me about your problem or describe what you'd like to analyze..."
required
/>
<button
id="sendChatBtn"
type="submit"
class="btn btn-primary"
>
📤 Send
</button>
</div>
</form>
<!-- Report Generation Section -->
<div id="reportSection" style="margin-top: var(--spacing-lg);">
<h4 class="section-subtitle">📋 Generate Final Report</h4>
<div id="reportNotReady" class="info-message">
💬 Ask 2-3 questions about your problem first, then I'll generate a comprehensive report
</div>
<div id="reportReady" class="success-message" style="display: none;">
✅ Ready! I can now generate a detailed 5 Whys analysis report based on our conversation
</div>
<button
id="generateReportBtn"
onclick="generateReport()"
class="btn btn-primary"
disabled
style="width: 100%;"
>
🔍 Generate Report ({{ agent.price }} AED)
</button>
</div>
<!-- Generated Report Display -->
<div id="reportResults" class="results-card" style="display: none;">
<div class="results-header">
<div style="font-size: 24px;">📊</div>
<h3 style="font-size: 20px; font-weight: 600; color: var(--text-primary); margin: 0;">5 Whys Analysis Report</h3>
<div style="background: var(--primary-color); color: white; padding: 6px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; margin-left: auto;">✅ Complete</div>
</div>
<div class="results-content" id="reportContent">
<!-- Report content will be displayed here -->
</div>
<div class="action-buttons">
<button onclick="copyReport()" class="btn btn-primary">📋 Copy Report</button>
<button onclick="downloadReport()" class="btn btn-secondary">💾 Download Report</button>
</div>
</div>
</div>
</div>
<!-- How It Works Widget -->
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon"></span>
How It Works
</h3>
</div>
<div class="widget-content">
<ol class="info-list">
<li>Chat freely to explore your problem</li>
<li>Get guidance and ask questions</li>
<li>Generate final report when ready</li>
<li>Pay only for the final report</li>
</ol>
<!-- Quick Agents Toggle Button -->
<button class="quick-agent-toggle" onclick="toggleQuickAgents()"
title="Quick access to other agents"
aria-label="Open quick access panel for other AI agents"
aria-expanded="false"
aria-controls="quickAgentsPanel"
style="margin-top: var(--spacing-md);">
<span class="toggle-icon" aria-hidden="true">🚀</span>
<span class="toggle-text">Explore Other Agents</span>
</button>
</div>
</div>
</div>
<!-- Processing Status -->
<div class="agent-grid">
{% 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..." %}
</div>
<!-- Results -->
<div class="agent-grid">
{% include "components/results_container.html" with results_title="5 Whys Analysis Report" %}
</div>
</div>
<style>
/* Five Whys Analyzer Specific Styles */
.chat-container {
background: var(--background-subtle);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
.welcome-message {
margin-bottom: 16px;
padding: 16px 20px;
background: var(--background-subtle);
border-radius: 16px 16px 16px 4px;
border-left: 4px solid var(--primary-color);
line-height: 1.6;
}
.message-header {
font-weight: 600;
color: var(--primary-color);
margin-bottom: 8px;
}
.chat-form {
border-top: 1px solid var(--border-color);
padding-top: 16px;
margin-top: 16px;
}
.input-group {
display: flex;
gap: 12px;
align-items: stretch;
}
.input-group .form-input {
width: 80%;
height: 40px;
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
font-family: inherit;
font-size: 14px;
background: var(--background-primary);
color: var(--text-primary);
box-sizing: border-box;
}
.input-group .form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.message {
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
.user-message {
margin-left: 20%;
padding: 12px 16px;
background: var(--primary-color);
color: white;
border-radius: 16px 16px 4px 16px;
}
.assistant-message {
margin-right: 20%;
padding: 16px 20px;
background: var(--background-subtle);
border-radius: 16px 16px 16px 4px;
border-left: 4px solid var(--primary-color);
line-height: 1.6;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.typing-dots {
display: flex;
gap: 4px;
align-items: center;
}
.typing-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-color);
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
</style>
<script>
let currentSessionId = null;
let isProcessing = false;
let messageCount = 0;
// Initialize session
document.addEventListener('DOMContentLoaded', function() {
// Generate new session ID
currentSessionId = generateSessionId();
// Handle chat form submission
document.getElementById('chatForm').addEventListener('submit', function(e) {
e.preventDefault();
sendChatMessage();
});
// Add Enter key support for chat input (Shift+Enter for new line)
document.getElementById('chatInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendChatMessage();
}
});
});
function generateSessionId() {
// Use crypto.randomUUID() if available, fallback to secure random generation
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
} else {
// Fallback for older browsers - generate cryptographically secure random string
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return 'session_' + Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
}
function getCsrfToken() {
return document.querySelector('[name=csrfmiddlewaretoken]').value;
}
function sendChatMessage() {
if (isProcessing) return;
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message) {
showToast('Please enter a message', 'error');
return;
}
isProcessing = true;
document.getElementById('sendChatBtn').disabled = true;
document.getElementById('sendChatBtn').textContent = 'Sending...';
// Add user message to chat
addMessageToChat(message, 'user');
input.value = '';
messageCount++;
// Show typing indicator
showTypingIndicator();
// Send chat message to backend
fetch("{% url 'five_whys_analyzer:chat' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
session_id: currentSessionId,
message: message
})
})
.then(response => response.json())
.then(data => {
// Hide typing indicator
hideTypingIndicator();
if (data.success) {
// Add assistant response to chat
addMessageToChat(data.response, 'assistant');
currentSessionId = data.session_id;
// Check if report button should be enabled
checkReportReadiness();
} else {
showToast(data.error || 'Failed to send message', 'error');
}
})
.catch(error => {
// Hide typing indicator on error
hideTypingIndicator();
console.error('Error:', error);
showToast('Network error occurred', 'error');
})
.finally(() => {
isProcessing = false;
document.getElementById('sendChatBtn').disabled = false;
document.getElementById('sendChatBtn').textContent = 'Send';
});
}
function addMessageToChat(message, role) {
const messagesContainer = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
if (role === 'user') {
messageDiv.className = 'message user-message';
messageDiv.innerHTML = `
<div style="font-weight: 600; margin-bottom: 4px;">You</div>
<div style="line-height: 1.5;">${escapeHtml(message)}</div>
`;
} else {
messageDiv.className = 'message assistant-message';
const formattedMessage = formatAssistantMessage(message);
messageDiv.innerHTML = `
<div style="font-weight: 600; color: #4338ca; margin-bottom: 8px;">🔍 5 Whys Analyst</div>
<div class="message-content">${formattedMessage}</div>
`;
}
messagesContainer.appendChild(messageDiv);
// Scroll to bottom
const chatContainer = document.getElementById('chatContainer');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function formatAssistantMessage(message) {
// Trim and clean up the message
let cleaned = message.trim();
// Remove excessive spacing and normalize line breaks
cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n'); // Max 2 line breaks
// Escape HTML first
let formatted = escapeHtml(cleaned);
// Convert markdown-style formatting to HTML
// Convert ### headers to h3
formatted = formatted.replace(/### (.*?)(?=\n|$)/g, '<h3>$1</h3>');
// Convert ** bold ** to <strong>
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Convert - bullet points to proper lists
const lines = formatted.split('\n');
let inList = false;
let result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('- ')) {
if (!inList) {
result.push('<ul>');
inList = true;
}
result.push(`<li>${line.substring(2)}</li>`);
} else {
if (inList) {
result.push('</ul>');
inList = false;
}
if (line) {
// Split long paragraphs for better readability
if (line.length > 200) {
const sentences = line.split('. ');
let currentParagraph = '';
for (const sentence of sentences) {
if (currentParagraph.length + sentence.length > 200 && currentParagraph) {
result.push(`<p>${currentParagraph.trim()}.</p>`);
currentParagraph = sentence;
} else {
currentParagraph += (currentParagraph ? '. ' : '') + sentence;
}
}
if (currentParagraph) {
result.push(`<p>${currentParagraph}</p>`);
}
} else {
result.push(`<p>${line}</p>`);
}
}
}
}
if (inList) {
result.push('</ul>');
}
return result.join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showTypingIndicator() {
const messagesContainer = document.getElementById('chatMessages');
const typingDiv = document.createElement('div');
typingDiv.id = 'typingIndicator';
typingDiv.className = 'message assistant-message';
typingDiv.innerHTML = `
<div style="font-weight: 600; color: #4338ca; margin-bottom: 8px;">🔍 5 Whys Analyst</div>
<div class="message-content">
<div style="display: flex; align-items: center; gap: 8px;">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
<span style="color: #6b7280; font-style: italic;">Analyzing your problem...</span>
</div>
</div>
`;
messagesContainer.appendChild(typingDiv);
// Scroll to bottom
const chatContainer = document.getElementById('chatContainer');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function hideTypingIndicator() {
const typingIndicator = document.getElementById('typingIndicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
function checkReportReadiness() {
if (messageCount >= 2) {
// Enable report generation
document.getElementById('reportNotReady').style.display = 'none';
document.getElementById('reportReady').style.display = 'block';
const btn = document.getElementById('generateReportBtn');
btn.disabled = false;
}
}
function generateReport() {
if (isProcessing) return;
if (!currentSessionId) {
showToast('Please start a chat session first', 'error');
return;
}
if (messageCount < 2) {
showToast('Please ask at least 2 questions before generating a report', 'error');
return;
}
isProcessing = true;
const btn = document.getElementById('generateReportBtn');
btn.disabled = true;
btn.textContent = '🔍 Generating Report...';
// Extract problem statement from first user message in chat
const chatMessages = document.querySelectorAll('.user-message');
let problemStatement = 'Problem analysis based on chat conversation';
if (chatMessages.length > 0) {
const firstMessage = chatMessages[0].querySelector('div:last-child');
if (firstMessage) {
problemStatement = firstMessage.textContent.trim();
}
}
// Send report generation request using chat history
fetch("{% url 'five_whys_analyzer:report' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
session_id: currentSessionId,
problem_statement: problemStatement,
context_info: 'Generated from chat conversation',
analysis_depth: 'comprehensive'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Display the generated report
displayReport(data.report);
// Update wallet balance if provided
if (data.wallet_balance !== undefined) {
updateWalletBalance(data.wallet_balance);
}
showToast('✅ 5 Whys analysis completed successfully!', 'success');
} else {
showToast(data.error || 'Failed to generate report', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showToast('Network error occurred', 'error');
})
.finally(() => {
isProcessing = false;
btn.disabled = false;
btn.textContent = '🔍 Generate Report ({{ agent.price }} AED)';
});
}
function displayReport(reportContent) {
const formattedReport = formatReportContent(reportContent);
document.getElementById('reportContent').innerHTML = formattedReport;
document.getElementById('reportResults').style.display = 'block';
// Scroll to report
document.getElementById('reportResults').scrollIntoView({ behavior: 'smooth' });
}
function formatReportContent(content) {
// Basic formatting for better readability
let formatted = content;
// Escape HTML first
const div = document.createElement('div');
div.textContent = formatted;
formatted = div.innerHTML;
// Format main headers
formatted = formatted.replace(/^# (.*?)$/gm, '<h1 style="font-size: 24px; font-weight: bold; margin: 20px 0 16px 0; color: #1f2937; border-bottom: 2px solid #6366f1; padding-bottom: 8px;">$1</h1>');
// Format section headers
formatted = formatted.replace(/^## (.*?)$/gm, '<h2 style="font-size: 18px; font-weight: 600; margin: 24px 0 12px 0; color: #374151;">$1</h2>');
// Format bold text
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Format bullet points
formatted = formatted.replace(/^- (.*?)$/gm, '<div style="margin: 6px 0; padding-left: 16px;">• $1</div>');
// Format numbered lists
formatted = formatted.replace(/^\d+\.\s+(.*?)$/gm, '<div style="margin: 6px 0;">$&</div>');
// Add line breaks for paragraphs
formatted = formatted.replace(/\n\n/g, '<br><br>');
formatted = formatted.replace(/\n/g, '<br>');
return formatted;
}
function copyReport() {
const reportText = generateTextForExport('reportContent');
navigator.clipboard.writeText(reportText).then(() => {
showToast('📋 Copied to clipboard!', 'success');
}).catch(() => {
showToast('❌ Failed to copy to clipboard', 'error');
});
}
function downloadReport() {
const reportText = generateTextForExport('reportContent');
const blob = new Blob([reportText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `five-whys-analysis-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
// No toast for download - file download is confirmation enough
}
function generateTextForExport(elementId) {
const element = document.getElementById(elementId);
if (!element) return '';
// Extract text content while preserving some structure
let text = element.innerText || element.textContent || '';
// Clean up extra whitespace
text = text.replace(/\n\s*\n\s*\n/g, '\n\n');
text = text.trim();
return text;
}
</script>
{% endblock %}

View File

@ -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/<str:session_id>/', views.five_whys_analyzer_session, name='session'),
# Legacy compatibility
path('process/', views.FiveWhysAnalyzerProcessView.as_view(), name='process'),
]

View File

@ -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 '<script' in message.lower() or '<iframe' in message.lower():
errors.append("Invalid characters in message")
elif validation_type == "report":
problem_statement = data.get('problem_statement', '').strip()
context_info = data.get('context_info', '').strip()
analysis_depth = data.get('analysis_depth', 'standard')
if not problem_statement:
errors.append("Problem statement is required")
elif len(problem_statement) > 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)

View File

@ -1 +0,0 @@
# Job Posting Generator Agent App

View File

@ -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']

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class JobPostingGeneratorConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'job_posting_generator'

View File

@ -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',
},
),
]

View File

@ -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'

View File

@ -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 roles 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"
}
]
}

View File

@ -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

View File

@ -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}")

View File

@ -1,764 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Job Posting Generator Agent - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<!-- Security: Content Security Policy -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; img-src 'self' data:; connect-src 'self';">
{% endblock %}
{% block content %}
<script>
// Set user authentication status for JavaScript access
document.addEventListener('DOMContentLoaded', function() {
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-login-url', '{% url "authentication:login" %}');
document.body.setAttribute('data-wallet-url', '{% url "wallet:wallet" %}');
// Initialize agent configuration
window.AGENT_PRICE = parseFloat('{{ agent.price }}');
});
</script>
<script>
// Agent Frontend Template - Common JavaScript Utilities
// Quick Agent Access Panel Functions
function toggleQuickAgents() {
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
const toggle = document.querySelector('.quick-agent-toggle');
const isActive = panel.classList.contains('active');
if (isActive) {
// Close panel
panel.classList.remove('active');
overlay.classList.remove('active');
toggle.classList.remove('active');
toggle.setAttribute('aria-expanded', 'false');
panel.setAttribute('aria-hidden', 'true');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = 'auto';
} else {
// Open panel
panel.classList.add('active');
overlay.classList.add('active');
toggle.classList.add('active');
toggle.setAttribute('aria-expanded', 'true');
panel.setAttribute('aria-hidden', 'false');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
}
function closeQuickAgents() {
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
const toggle = document.querySelector('.quick-agent-toggle');
if (panel && overlay && toggle) {
panel.classList.remove('active');
overlay.classList.remove('active');
toggle.classList.remove('active');
toggle.setAttribute('aria-expanded', 'false');
panel.setAttribute('aria-hidden', 'true');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = 'auto';
}
}
// Toast Notification Function
function showToast(message, type = 'info') {
// Remove existing toasts
document.querySelectorAll('.toast').forEach(toast => toast.remove());
// Create new toast
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
// Add to page
document.body.appendChild(toast);
// Show toast with animation
setTimeout(() => toast.classList.add('show'), 100);
// Auto remove after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Processing Status Functions
function showProcessing() {
const processingStatus = document.getElementById('processingStatus');
const resultsContainer = document.getElementById('resultsContainer');
if (processingStatus) {
processingStatus.style.display = 'block';
processingStatus.classList.add('active');
// Smooth scroll to processing section
processingStatus.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
if (resultsContainer) resultsContainer.style.display = 'none';
}
function hideProcessing() {
const processingStatus = document.getElementById('processingStatus');
processingStatus.style.display = 'none';
processingStatus.classList.remove('active');
}
// Wallet Balance Update Function
function updateWalletBalance(newBalance) {
if (newBalance !== undefined) {
const walletBalance = document.getElementById('walletBalance');
if (walletBalance) {
walletBalance.textContent = newBalance.toFixed(2);
}
}
}
// Copy to Clipboard Utility
function copyToClipboard(text, successMessage = 'Copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
showToast(`📋 ${successMessage}`, 'success');
}).catch(() => {
showToast('Failed to copy to clipboard', 'error');
});
}
// Download as File Utility
function downloadAsFile(text, filename, successMessage = 'File downloaded!') {
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `content-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
showToast(`💾 ${successMessage}`, 'success');
}
// Close panel on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeQuickAgents();
}
});
// Security: HTML escaping function
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Security: Safe DOM content creation
function createSecureElement(tagName, className, textContent) {
const element = document.createElement(tagName);
if (className) element.className = className;
if (textContent) element.textContent = textContent;
return element;
}
// Initialize accessibility features
document.addEventListener('DOMContentLoaded', function() {
// Set initial ARIA states
const quickAgentsButton = document.querySelector('.quick-agent-toggle');
if (quickAgentsButton) {
quickAgentsButton.setAttribute('aria-expanded', 'false');
}
const panel = document.getElementById('quickAgentsPanel');
const overlay = document.getElementById('quickAgentsOverlay');
if (panel) panel.setAttribute('aria-hidden', 'true');
if (overlay) overlay.setAttribute('aria-hidden', 'true');
});
// Job Posting specific utilities
const JobPostingUtils = {
renderSecureContent(container, content) {
// Secure rendering without innerHTML
const wrapper = document.createElement('div');
wrapper.className = 'results-content';
// Basic text processing for job postings
const lines = content.split('\n');
lines.forEach(line => {
if (line.trim()) {
const p = document.createElement('p');
p.textContent = line.trim();
wrapper.appendChild(p);
}
});
container.appendChild(wrapper);
},
copyJobPosting() {
const content = document.getElementById('jobContent');
if (content) {
const text = content.textContent || content.innerText || '';
copyToClipboard(text, 'Job posting copied to clipboard!');
}
},
downloadJobPosting() {
const content = document.getElementById('jobContent');
if (content) {
const text = content.textContent || content.innerText || '';
downloadAsFile(text, `job-posting-${Date.now()}.txt`, 'Job posting downloaded!');
}
}
};
</script>
<div class="agent-container">
<!-- Agent Header -->
{% include "components/agent_header.html" with agent_title="Job Posting Generator" agent_subtitle="Create professional job postings with AI-powered content generation" %}
<!-- Quick Agent Access Panel -->
{% include "components/quick_agents_panel.html" %}
<!-- Messages -->
{% if messages %}
{% for message in messages %}
<div class="{% if message.tags == 'error' %}alert alert-error{% else %}alert{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- Agent Grid -->
<div class="agent-grid">
<div class="agent-widget widget-large" style="flex: 1; margin-right: var(--spacing-lg);">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon">📝</span>
Job Posting Form
</h3>
</div>
<div class="widget-content">
<form method="POST" id="jobPostingForm">
{% csrf_token %}
<div class="form-group">
<label class="form-label" for="job_title">Job Title *</label>
<input type="text" name="job_title" id="job_title" class="form-input" placeholder="e.g., Senior Software Engineer" required>
</div>
<div class="form-group">
<label class="form-label" for="company_name">Company Name *</label>
<input type="text" name="company_name" id="company_name" class="form-input" placeholder="e.g., TechCorp Inc." required>
</div>
<div class="form-group">
<label class="form-label" for="job_description">Job Description *</label>
<textarea name="job_description" id="job_description" class="form-textarea" placeholder="Describe the role, requirements, and company culture" rows="4" required></textarea>
</div>
<div class="form-group">
<label class="form-label" for="seniority_level">Seniority Level *</label>
<select name="seniority_level" id="seniority_level" class="form-input" required>
<option value="">Select level...</option>
<option value="entry">Entry Level</option>
<option value="mid">Mid Level</option>
<option value="senior">Senior Level</option>
<option value="lead">Lead/Principal</option>
<option value="executive">Executive</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="contract_type">Contract Type *</label>
<select name="contract_type" id="contract_type" class="form-input" required>
<option value="">Select type...</option>
<option value="full-time">Full-time</option>
<option value="part-time">Part-time</option>
<option value="contract">Contract</option>
<option value="freelance">Freelance</option>
<option value="internship">Internship</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="location">Location *</label>
<input type="text" name="location" id="location" class="form-input" placeholder="e.g., Dubai, UAE or Remote" required>
</div>
<div class="form-group">
<label class="form-label" for="language">Language</label>
<select name="language" id="language" class="form-input">
<option value="English">English</option>
<option value="Arabic">Arabic</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
<option value="German">German</option>
</select>
</div>
{% if user.is_authenticated %}
{% if user.wallet_balance >= agent.price %}
<button type="submit" class="btn btn-primary btn-full" id="processButton">
💼 Generate Job Posting ({{ agent.price }} AED)
</button>
{% else %}
<div class="alert alert-error">
Insufficient balance! You need {{ agent.price }} AED.
</div>
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full">
💰 Top Up Wallet
</a>
{% endif %}
{% else %}
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
🔑 Login to Continue
</a>
{% endif %}
</form>
</div>
</div>
<!-- How It Works Widget -->
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
<div class="widget-header">
<h3 class="widget-title">
<span class="widget-icon"></span>
How It Works
</h3>
</div>
<div class="widget-content">
<ol class="info-list">
<li>Enter job requirements</li>
<li>Configure position details</li>
<li>Process with AI</li>
<li>Get professional posting</li>
</ol>
<!-- Quick Agents Toggle Button -->
<button class="quick-agent-toggle" onclick="toggleQuickAgents()"
title="Quick access to other agents"
aria-label="Open quick access panel for other AI agents"
aria-expanded="false"
aria-controls="quickAgentsPanel"
style="margin-top: var(--spacing-md);">
<span class="toggle-icon" aria-hidden="true">🚀</span>
<span class="toggle-text">Explore Other Agents</span>
</button>
</div>
</div>
</div>
<!-- Processing Status -->
<div class="agent-grid">
{% include "components/processing_status.html" with status_title="Creating Job Posting..." status_text="Please wait while we generate your professional job posting..." %}
</div>
<!-- Results -->
<div class="agent-grid">
{% include "components/results_container.html" with results_title="Generated Job Posting" %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Form validation
function isFormValid() {
const required = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location'];
return required.every(id => document.getElementById(id).value.trim());
}
// Display results with secure formatting
function displayResults(result) {
const resultsContainer = document.getElementById('resultsContainer');
const contentElement = document.getElementById('resultsContent');
const processingStatus = document.getElementById('processingStatus');
if (result.success) {
// Hide processing status
hideProcessing();
// Update wallet balance if provided
if (result.wallet_balance !== undefined) {
updateWalletBalance(result.wallet_balance);
}
// Get job posting content
const jobContent = result.job_posting_content || result.content || 'Job posting generated successfully!';
if (contentElement) {
// Clear existing content safely
contentElement.textContent = '';
// Create secure container
const jobContainer = createSecureElement('div', 'job-posting-content');
// Parse content securely line by line
const lines = jobContent.split('\n');
let currentParagraph = null;
let currentList = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) {
// Empty line - end current paragraph/list
if (currentParagraph) {
jobContainer.appendChild(currentParagraph);
currentParagraph = null;
}
if (currentList) {
jobContainer.appendChild(currentList);
currentList = null;
}
continue;
}
// Check for headers (markdown style **text**)
const headerMatch = line.match(/^\*\*([^*]+)\*\*$/);
if (headerMatch) {
// End current elements
if (currentParagraph) {
jobContainer.appendChild(currentParagraph);
currentParagraph = null;
}
if (currentList) {
jobContainer.appendChild(currentList);
currentList = null;
}
// Create secure header
const header = createSecureElement('h3', 'job-section-title', headerMatch[1]);
jobContainer.appendChild(header);
continue;
}
// Check for bullet points
const bulletMatch = line.match(/^-\s+(.+)$/);
if (bulletMatch) {
// End current paragraph
if (currentParagraph) {
jobContainer.appendChild(currentParagraph);
currentParagraph = null;
}
// Create or continue list
if (!currentList) {
currentList = createSecureElement('ul', 'job-list');
}
const listItem = createSecureElement('li', null, bulletMatch[1]);
currentList.appendChild(listItem);
continue;
}
// Regular text - add to paragraph
if (currentList) {
jobContainer.appendChild(currentList);
currentList = null;
}
if (!currentParagraph) {
currentParagraph = createSecureElement('p', 'job-paragraph');
} else {
// Add line break for multi-line paragraphs
currentParagraph.appendChild(document.createElement('br'));
}
currentParagraph.appendChild(document.createTextNode(line));
}
// Add any remaining elements
if (currentParagraph) {
jobContainer.appendChild(currentParagraph);
}
if (currentList) {
jobContainer.appendChild(currentList);
}
// Safely append to DOM
contentElement.appendChild(jobContainer);
}
// Show results container with animation
if (resultsContainer) {
resultsContainer.style.display = 'block';
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// No toast needed - results display is confirmation enough
} else {
// Handle error case
hideProcessing();
const errorMsg = result.error || result.error_message || 'Failed to generate job posting';
showToast(`❌ ${escapeHtml(errorMsg)}`, 'error');
}
}
// Poll for results
function pollForResults(requestId) {
let pollCount = 0;
const maxPolls = 30; // 30 seconds max
console.log(`Starting polling for request: ${requestId}`);
const pollInterval = setInterval(() => {
pollCount++;
console.log(`Poll attempt ${pollCount}/${maxPolls} for request: ${requestId}`);
fetch(`/agents/job-posting-generator/status/${requestId}/`)
.then(response => {
console.log(`Status response: ${response.status}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(result => {
console.log('Poll result:', result);
if (result.status === 'completed') {
clearInterval(pollInterval);
hideProcessing();
document.getElementById('processButton').disabled = false;
displayResults(result);
} else if (result.status === 'failed') {
clearInterval(pollInterval);
hideProcessing();
document.getElementById('processButton').disabled = false;
showToast(`❌ Processing failed: ${result.error || 'Unknown error'}`, 'error');
} else if (pollCount >= maxPolls) {
clearInterval(pollInterval);
hideProcessing();
document.getElementById('processButton').disabled = false;
showToast('⏰ Processing is taking longer than expected. Please check back later.', 'error');
}
// Continue polling if status is 'processing' or 'pending'
})
.catch(error => {
console.error('Polling error:', error);
clearInterval(pollInterval);
hideProcessing();
document.getElementById('processButton').disabled = false;
showToast(`❌ Error checking status: ${error.message}`, 'error');
});
}, 1000);
}
// Handle form submission
document.getElementById('jobPostingForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!isFormValid()) {
showToast('Please fill in all required fields', 'error');
return;
}
const processButton = document.getElementById('processButton');
if (processButton.disabled) return;
const isAuthenticated = document.body.getAttribute('data-user-authenticated') === 'true';
if (!isAuthenticated) {
window.location.href = document.body.getAttribute('data-login-url');
return;
}
const currentBalance = parseFloat(document.getElementById('walletBalance').textContent) || 0;
const requiredBalance = window.AGENT_PRICE || {{ agent.price }};
if (currentBalance < requiredBalance) {
showToast(`Insufficient balance! You need ${requiredBalance} AED.`, 'error');
setTimeout(() => window.location.href = document.body.getAttribute('data-wallet-url'), 2000);
return;
}
showProcessing();
processButton.disabled = true;
const resultsContainer = document.getElementById('resultsContainer');
if (resultsContainer) resultsContainer.style.display = 'none';
fetch(window.location.href, {
method: 'POST',
body: new FormData(this),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(response => response.json())
.then(result => {
if (result.success && result.request_id) {
pollForResults(result.request_id);
} else {
hideProcessing();
processButton.disabled = false;
displayResults(result);
}
})
.catch(error => {
hideProcessing();
processButton.disabled = false;
showToast('❌ Network error', 'error');
});
});
// Enhanced copy and download functions
function copyResults() {
const content = document.getElementById('resultsContent');
if (content) {
// Get clean text content without HTML formatting
const text = content.textContent || content.innerText || '';
navigator.clipboard.writeText(text).then(() => {
// No toast for successful copy - clipboard action is confirmation enough
}).catch(() => {
showToast('❌ Failed to copy to clipboard', 'error');
});
} else {
showToast('❌ No job posting content to copy', 'error');
}
}
function downloadResults() {
const content = document.getElementById('resultsContent');
if (content) {
// Get clean text content without HTML formatting
const text = content.textContent || content.innerText || '';
// Create filename with current date and job title if available
const jobTitle = document.getElementById('job_title')?.value || 'job-posting';
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
const filename = `${jobTitle.toLowerCase().replace(/\s+/g, '-')}-${timestamp}.txt`;
// Create and download file
const blob = new Blob([text], { type: 'text/plain; charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// No toast for download - file download is confirmation enough
} else {
showToast('❌ No job posting content to download', 'error');
}
}
function resetForm() {
document.getElementById('jobPostingForm').reset();
const resultsContainer = document.getElementById('resultsContainer');
if (resultsContainer) resultsContainer.style.display = 'none';
document.getElementById('processButton').disabled = false;
// Scroll back to form for new input
const formSection = document.getElementById('jobPostingForm');
if (formSection) {
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// No toast for reset - visual feedback is enough
}
</script>
<style>
/* Job Posting Results Styling */
.job-posting-content {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--text-primary);
max-width: none;
}
.job-section-title {
color: var(--primary-color);
font-size: 1.25rem;
font-weight: 600;
margin: 1.5rem 0 0.75rem 0 !important;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color);
}
.job-section-title:first-child {
margin-top: 0 !important;
}
.job-paragraph {
margin: 1rem 0;
text-align: justify;
color: var(--text-primary);
}
.job-list {
margin: 1rem 0;
padding-left: 1.5rem;
}
.job-list li {
margin: 0.5rem 0;
line-height: 1.5;
color: var(--text-primary);
}
.job-list li::marker {
color: var(--primary-color);
}
/* Enhanced results container */
#resultsContainer .results-content {
background: var(--background-subtle);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--border-color);
}
/* Action buttons styling */
.results-actions {
margin-top: 1.5rem;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.results-actions .btn {
flex: 1;
min-width: 140px;
}
/* Responsive design */
@media (max-width: 768px) {
.job-section-title {
font-size: 1.1rem;
}
.results-actions {
flex-direction: column;
}
.results-actions .btn {
flex: none;
width: 100%;
}
}
/* Loading animation for smooth transitions */
#resultsContainer {
transition: opacity 0.3s ease-in-out;
}
#resultsContainer[style*="display: none"] {
opacity: 0;
}
#resultsContainer[style*="display: block"] {
opacity: 1;
}
</style>
{% endblock %}

View File

@ -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/<uuid:request_id>/', views.job_posting_generator_result, name='status'),
]

View File

@ -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)

View File

@ -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',

View File

@ -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')),

View File

@ -1 +0,0 @@
# Social Ads Generator Agent App

View File

@ -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']

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class SocialAdsGeneratorConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'social_ads_generator'

View File

@ -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',
},
),
]

View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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 = [
'<script',
'javascript:',
'onclick=',
'onerror=',
'onload=',
'eval(',
'document.cookie',
'window.location'
]
for pattern in malicious_patterns:
if pattern.lower() in content.lower():
return "Content filtered for security reasons"
# Basic content quality check
if len(content.strip()) < 10:
return "Generated content too short - please try again"
return content.strip()

File diff suppressed because it is too large Load Diff

View File

@ -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/<uuid:request_id>/', views.social_ads_generator_status, name='status'),
]

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More