mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 14:12:57 +00:00
🗑️ 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:
parent
73d514152c
commit
ec42b51d7f
@ -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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# Agent Base Framework
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -1 +0,0 @@
|
||||
# {{ agent_name }} Agent App
|
||||
@ -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']
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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 }}'
|
||||
@ -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'),
|
||||
]
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -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}")
|
||||
@ -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"
|
||||
)
|
||||
)
|
||||
@ -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!")
|
||||
@ -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)
|
||||
@ -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/")
|
||||
@ -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}")
|
||||
@ -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"
|
||||
)
|
||||
)
|
||||
@ -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"))
|
||||
@ -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")
|
||||
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -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'),
|
||||
]
|
||||
@ -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,
|
||||
})
|
||||
@ -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]
|
||||
}
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# Data Analysis Agent Agent App
|
||||
@ -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']
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DataAnalysisAgentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'data_analyzer'
|
||||
@ -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")
|
||||
)
|
||||
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
]
|
||||
@ -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/",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -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}")
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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}")
|
||||
@ -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 %}
|
||||
@ -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'),
|
||||
]
|
||||
@ -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)
|
||||
@ -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 ===
|
||||
@ -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
|
||||
@ -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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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}"
|
||||
@ -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
@ -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'),
|
||||
]
|
||||
@ -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)
|
||||
@ -1 +0,0 @@
|
||||
# 5 Whys Analysis Agent Agent App
|
||||
@ -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')
|
||||
}),
|
||||
)
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FiveWhysAnalyzerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'five_whys_analyzer'
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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
@ -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
|
||||
@ -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')
|
||||
@ -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 %}
|
||||
@ -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'),
|
||||
]
|
||||
@ -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)
|
||||
@ -1 +0,0 @@
|
||||
# Job Posting Generator Agent App
|
||||
@ -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']
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class JobPostingGeneratorConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'job_posting_generator'
|
||||
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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'
|
||||
@ -1,300 +0,0 @@
|
||||
{
|
||||
"name": "Job Posting Generator",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "list",
|
||||
"value": "gpt-4o",
|
||||
"cachedResultName": "gpt-4o"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "8bc1629f-d935-4fa8-bbb9-b55403207400",
|
||||
"name": "OpenAI Chat Model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"position": [
|
||||
968,
|
||||
1020
|
||||
],
|
||||
"typeVersion": 1.2,
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "uzyuJ5c9nml2NneC",
|
||||
"name": "OpenAi account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sessionIdType": "customKey",
|
||||
"sessionKey": "={{ $('Set Web Input').item.json.body.sessionId }}",
|
||||
"contextWindowLength": 50
|
||||
},
|
||||
"id": "93a19f8c-f3e3-4094-bbe7-019bcb5bdd0e",
|
||||
"name": "Simple Memory",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"position": [
|
||||
1088,
|
||||
1020
|
||||
],
|
||||
"typeVersion": 1.3
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.body.message.text }}",
|
||||
"options": {
|
||||
"systemMessage": "=You are an expert recruitment copywriter. Your task is to craft engaging and compelling job postings that attract top talent. For each job posting, use the provided input details (such as job title, responsibilities, qualifications, company information, and benefits) to:\n\nWrite a clear and enticing job title.\n\nSummarize the company and its culture in a way that excites candidates.\n\nClearly describe the role’s responsibilities and day-to-day tasks.\n\nList the Job title, About us, Job Overview, Responsibilities, required qualifications and preferred skills, Location and How to Apply in an appealing, concise manner.\n\nHighlight unique benefits and growth opportunities.\n\nUse inclusive, positive, and motivating language throughout.\n\nEnsure the posting is well-structured, easy to read, and free of jargon.\n\nYour goal is to make each job posting stand out and appeal to high-quality candidates, while accurately reflecting the role and company."
|
||||
}
|
||||
},
|
||||
"id": "1838d72d-12da-4351-beea-8625f60ff88d",
|
||||
"name": "AI Agent",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"position": [
|
||||
940,
|
||||
800
|
||||
],
|
||||
"typeVersion": 1.9
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"chatId": "={{$('Telegram Trigger').first().json.message.chat.id}}",
|
||||
"text": "={{ $json.output }}",
|
||||
"additionalFields": {
|
||||
"appendAttribution": false
|
||||
}
|
||||
},
|
||||
"id": "ce2d4d37-c3cb-4dd0-9b70-9e1db9830e74",
|
||||
"name": "Send Response To Telegram",
|
||||
"type": "n8n-nodes-base.telegram",
|
||||
"position": [
|
||||
500,
|
||||
440
|
||||
],
|
||||
"webhookId": "702bcdca-5297-4faf-9759-4f570d127052",
|
||||
"typeVersion": 1.2,
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "43f84411-eaaa-488c-9b1f-856e90d0aaf6",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
500,
|
||||
800
|
||||
],
|
||||
"id": "a02855f5-0b5c-47de-b098-19cd10932d88",
|
||||
"webhookId": "43f84411-eaaa-488c-9b1f-856e90d0aaf6"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"name": "Set Web Input",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
720,
|
||||
800
|
||||
],
|
||||
"id": "8c87b34b-9119-4f29-baea-9a6b74efc937"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"name": "Respond to Web",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1316,
|
||||
800
|
||||
],
|
||||
"id": "e8ae02fc-93c3-476e-b01c-e60656ccfaac"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"formTitle": "Job Posting",
|
||||
"formFields": {
|
||||
"values": [
|
||||
{
|
||||
"fieldLabel": "Job title"
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Company Name"
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Describe what you'd like to generate",
|
||||
"fieldType": "textarea"
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Seniority",
|
||||
"fieldType": "dropdown",
|
||||
"fieldOptions": {
|
||||
"values": [
|
||||
{
|
||||
"option": "Junior"
|
||||
},
|
||||
{
|
||||
"option": "Mid-level"
|
||||
},
|
||||
{
|
||||
"option": "Senior"
|
||||
},
|
||||
{
|
||||
"option": "Lead"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Contract Type",
|
||||
"fieldType": "dropdown",
|
||||
"fieldOptions": {
|
||||
"values": [
|
||||
{
|
||||
"option": "Full-Time"
|
||||
},
|
||||
{
|
||||
"option": "Part-Time"
|
||||
},
|
||||
{
|
||||
"option": "Freelance"
|
||||
},
|
||||
{
|
||||
"option": "Internship"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Location",
|
||||
"fieldType": "dropdown",
|
||||
"fieldOptions": {
|
||||
"values": [
|
||||
{
|
||||
"option": "Remote"
|
||||
},
|
||||
{
|
||||
"option": "On-Site"
|
||||
},
|
||||
{
|
||||
"option": "Hybrid"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Language"
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Company Website"
|
||||
},
|
||||
{
|
||||
"fieldLabel": "How to Apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.formTrigger",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
500,
|
||||
180
|
||||
],
|
||||
"id": "0ad79a28-0909-4b88-bba0-e013cf4eae6d",
|
||||
"name": "On form submission",
|
||||
"webhookId": "75ac3236-9040-478a-88b4-e0bcce17fdf1",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"AI Agent": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Web",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Simple Memory": {
|
||||
"ai_memory": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"OpenAI Chat Model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set Web Input",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set Web Input": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"On form submission": {
|
||||
"main": [
|
||||
[]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "b0e25b31-2e02-4d60-9ee3-4b512dc25fad",
|
||||
"meta": {
|
||||
"instanceId": "b419dceeef095c7882b7f3bc7ba03f620c77ec1f3d9d0518174b97d631dd49fa"
|
||||
},
|
||||
"id": "nHrugmW7FvbKSlen",
|
||||
"tags": [
|
||||
{
|
||||
"createdAt": "2025-07-01T13:54:51.754Z",
|
||||
"updatedAt": "2025-07-01T13:54:51.754Z",
|
||||
"id": "2ji4EAexY8bmiTeM",
|
||||
"name": "AI Agent"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
@ -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}")
|
||||
@ -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 %}
|
||||
@ -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'),
|
||||
]
|
||||
@ -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)
|
||||
@ -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',
|
||||
|
||||
@ -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')),
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# Social Ads Generator Agent App
|
||||
@ -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']
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SocialAdsGeneratorConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'social_ads_generator'
|
||||
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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
@ -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'),
|
||||
]
|
||||
@ -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
Loading…
Reference in New Issue
Block a user