🚀 Complete agents app implementation with social ads frontend

- Create complete REST API-based agents system for scalability
- Implement Social Ads Generator with dynamic form rendering
- Add agents marketplace with search and category filtering
- Build real-time wallet balance updates after execution
- Fix URL routing conflicts and API endpoint issues
- Add comprehensive N8N webhook integration with proper payload format
- Create dynamic template system for 100+ agent scalability

Features:
- Database-driven agent management via Django admin
- JSON schema-based dynamic form generation
- Real-time wallet balance deduction and display updates
- Comprehensive error handling and validation
- Mobile-responsive marketplace UI
- Complete API endpoints for frontend integration

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2025-07-31 19:05:09 +05:30
parent 8097f6f4c7
commit 5eba8fee84
22 changed files with 2517 additions and 5 deletions

View File

@ -0,0 +1,639 @@
# Django Agents App Recreation Guide
This guide provides complete instructions for recreating the agents app in another Django project.
## Overview
Create a Django app called `agents` with the following functionality:
- Agent marketplace with categories
- Agent execution system with n8n webhook integration
- User balance checking and fee deduction
- Complete REST API with pagination
- Admin interface for management
## Installation Steps
### 1. Create the App
```bash
python manage.py startapp agents
```
### 2. Install Dependencies
```bash
pip install requests djangorestframework
```
### 3. Add to INSTALLED_APPS
In your `settings.py`:
```python
INSTALLED_APPS = [
# ... other apps
'rest_framework',
'agents',
]
```
### 4. Add to URLs
In your main `urls.py`:
```python
from django.urls import path, include
urlpatterns = [
# ... other URLs
path('api/agents/', include('agents.urls')),
]
```
## File Structure
```
agents/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── serializers.py
├── views.py
├── urls.py
├── migrations/
│ └── __init__.py
└── management/
└── commands/
└── create_sample_agents.py
```
## Code Files
### agents/models.py
```python
from django.db import models
import uuid
class AgentCategory(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
icon = models.CharField(max_length=50, blank=True, help_text="Icon class or emoji")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Agent(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
short_description = models.CharField(max_length=300)
description = models.TextField()
category = models.ForeignKey(AgentCategory, on_delete=models.CASCADE, related_name='agents')
price = models.DecimalField(max_digits=10, decimal_places=2)
form_schema = models.JSONField(help_text="JSON schema for agent input form")
webhook_url = models.URLField(help_text="n8n webhook URL for execution")
is_active = models.BooleanField(default=True)
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
class AgentExecution(models.Model):
STATUS_CHOICES = [
('pending', 'Pending'),
('running', 'Running'),
('completed', 'Completed'),
('failed', 'Failed'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name='executions')
user = models.ForeignKey('users.User', on_delete=models.CASCADE) # Adjust to your user model
input_data = models.JSONField()
output_data = models.JSONField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
fee_charged = models.DecimalField(max_digits=10, decimal_places=2)
webhook_response = models.JSONField(null=True, blank=True)
error_message = models.TextField(blank=True)
execution_time = models.DurationField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.agent.name} - {self.user.email} - {self.status}"
```
### agents/admin.py
```python
from django.contrib import admin
from .models import AgentCategory, Agent, AgentExecution
@admin.register(AgentCategory)
class AgentCategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Agent)
class AgentAdmin(admin.ModelAdmin):
list_display = ['name', 'category', 'price', 'is_active', 'created_at']
list_filter = ['category', 'is_active', 'created_at']
search_fields = ['name', 'description', 'short_description']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['created_at', 'updated_at']
@admin.register(AgentExecution)
class AgentExecutionAdmin(admin.ModelAdmin):
list_display = ['agent', 'user', 'status', 'fee_charged', 'created_at']
list_filter = ['status', 'created_at', 'agent__category']
search_fields = ['agent__name', 'user__email']
readonly_fields = ['created_at', 'completed_at']
```
### agents/serializers.py
```python
from rest_framework import serializers
from .models import Agent, AgentCategory, AgentExecution
class AgentCategorySerializer(serializers.ModelSerializer):
class Meta:
model = AgentCategory
fields = ['id', 'name', 'slug', 'description', 'icon']
class AgentSerializer(serializers.ModelSerializer):
category = AgentCategorySerializer(read_only=True)
class Meta:
model = Agent
fields = [
'id', 'name', 'slug', 'short_description', 'description',
'category', 'price', 'form_schema', 'created_at'
]
class AgentExecutionSerializer(serializers.ModelSerializer):
agent = AgentSerializer(read_only=True)
class Meta:
model = AgentExecution
fields = [
'id', 'agent', 'input_data', 'output_data', 'status',
'fee_charged', 'error_message', 'execution_time',
'created_at', 'completed_at'
]
```
### agents/views.py
```python
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django.shortcuts import get_object_or_404
from django.utils import timezone
from .models import Agent, AgentExecution
from .serializers import AgentSerializer, AgentExecutionSerializer
import requests
import json
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def agent_list(request):
"""List all active agents with optional category filtering"""
agents = Agent.objects.filter(is_active=True)
category = request.GET.get('category')
if category:
agents = agents.filter(category__slug=category)
search = request.GET.get('search')
if search:
agents = agents.filter(name__icontains=search)
paginator = PageNumberPagination()
paginator.page_size = 20
result_page = paginator.paginate_queryset(agents, request)
serializer = AgentSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def agent_detail(request, slug):
"""Get detailed agent information"""
agent = get_object_or_404(Agent, slug=slug, is_active=True)
serializer = AgentSerializer(agent)
return Response(serializer.data)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def execute_agent(request):
"""Execute an agent with provided input data"""
agent_slug = request.data.get('agent_slug')
input_data = request.data.get('input_data', {})
if not agent_slug:
return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
agent = get_object_or_404(Agent, slug=agent_slug, is_active=True)
# Check if user has sufficient balance (adjust based on your wallet system)
if hasattr(request.user, 'wallet_balance') and request.user.wallet_balance < agent.price:
return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
# Create execution record
execution = AgentExecution.objects.create(
agent=agent,
user=request.user,
input_data=input_data,
fee_charged=agent.price,
status='pending'
)
try:
# Deduct fee from user wallet (adjust based on your wallet system)
if hasattr(request.user, 'deduct_balance'):
request.user.deduct_balance(agent.price)
# Call n8n webhook
execution.status = 'running'
execution.save()
webhook_payload = {
'execution_id': str(execution.id),
'agent_slug': agent.slug,
'user_id': str(request.user.id),
'input_data': input_data
}
response = requests.post(
agent.webhook_url,
json=webhook_payload,
timeout=30
)
execution.webhook_response = response.json() if response.headers.get('content-type', '').startswith('application/json') else {'raw': response.text}
if response.status_code == 200:
execution.status = 'completed'
execution.output_data = execution.webhook_response
else:
execution.status = 'failed'
execution.error_message = f"Webhook returned {response.status_code}"
execution.completed_at = timezone.now()
execution.save()
serializer = AgentExecutionSerializer(execution)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except requests.RequestException as e:
execution.status = 'failed'
execution.error_message = str(e)
execution.completed_at = timezone.now()
execution.save()
return Response({
'error': 'Failed to execute agent',
'execution_id': str(execution.id)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def execution_list(request):
"""List user's agent executions"""
executions = AgentExecution.objects.filter(user=request.user)
paginator = PageNumberPagination()
paginator.page_size = 20
result_page = paginator.paginate_queryset(executions, request)
serializer = AgentExecutionSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def execution_detail(request, execution_id):
"""Get detailed execution information"""
execution = get_object_or_404(AgentExecution, id=execution_id, user=request.user)
serializer = AgentExecutionSerializer(execution)
return Response(serializer.data)
```
### agents/urls.py
```python
from django.urls import path
from . import views
urlpatterns = [
path('', views.agent_list, name='agent_list'),
path('<slug:slug>/', views.agent_detail, name='agent_detail'),
path('execute/', views.execute_agent, name='execute_agent'),
path('executions/', views.execution_list, name='execution_list'),
path('executions/<uuid:execution_id>/', views.execution_detail, name='execution_detail'),
]
```
### agents/management/commands/create_sample_agents.py
First create the directories:
```bash
mkdir -p agents/management/commands
touch agents/management/__init__.py
touch agents/management/commands/__init__.py
```
Then create the file:
```python
from django.core.management.base import BaseCommand
from agents.models import AgentCategory, Agent
class Command(BaseCommand):
help = 'Create sample agents for testing'
def handle(self, *args, **options):
# Create categories
ai_category, _ = AgentCategory.objects.get_or_create(
slug='ai-tools',
defaults={
'name': 'AI Tools',
'description': 'AI-powered automation tools',
'icon': '🤖'
}
)
data_category, _ = AgentCategory.objects.get_or_create(
slug='data-analysis',
defaults={
'name': 'Data Analysis',
'description': 'Data processing and analysis tools',
'icon': '📊'
}
)
web_category, _ = AgentCategory.objects.get_or_create(
slug='web-scraping',
defaults={
'name': 'Web Scraping',
'description': 'Web data extraction tools',
'icon': '🕷️'
}
)
# Create sample agents
Agent.objects.get_or_create(
slug='pdf-analyzer',
defaults={
'name': 'PDF Content Analyzer',
'short_description': 'Extract and analyze content from PDF documents',
'description': 'This agent processes PDF files and extracts meaningful insights including summaries, keywords, and sentiment analysis. Perfect for document processing workflows.',
'category': ai_category,
'price': 5.00,
'form_schema': {
'fields': [
{
'name': 'pdf_url',
'type': 'url',
'label': 'PDF URL',
'placeholder': 'https://example.com/document.pdf',
'required': True
},
{
'name': 'analysis_type',
'type': 'select',
'label': 'Analysis Type',
'options': [
{'value': 'summary', 'label': 'Summary'},
{'value': 'keywords', 'label': 'Keywords'},
{'value': 'sentiment', 'label': 'Sentiment Analysis'}
],
'required': True
}
]
},
'webhook_url': 'https://your-n8n-instance.com/webhook/pdf-analyzer'
}
)
Agent.objects.get_or_create(
slug='website-scraper',
defaults={
'name': 'Website Data Scraper',
'short_description': 'Extract structured data from any website',
'description': 'Advanced web scraping agent that can extract specific data from websites using CSS selectors or XPath. Handles JavaScript-rendered content and returns clean, structured data.',
'category': web_category,
'price': 3.00,
'form_schema': {
'fields': [
{
'name': 'website_url',
'type': 'url',
'label': 'Website URL',
'placeholder': 'https://example.com',
'required': True
},
{
'name': 'selectors',
'type': 'textarea',
'label': 'CSS Selectors (one per line)',
'placeholder': 'h1.title\n.price\n.description',
'required': True
},
{
'name': 'wait_for_js',
'type': 'checkbox',
'label': 'Wait for JavaScript to load',
'required': False
}
]
},
'webhook_url': 'https://your-n8n-instance.com/webhook/website-scraper'
}
)
Agent.objects.get_or_create(
slug='data-analyzer',
defaults={
'name': 'CSV Data Analyzer',
'short_description': 'Analyze and visualize CSV data with insights',
'description': 'Upload CSV files and get comprehensive data analysis including statistics, trends, and visualizations. Perfect for business intelligence and data exploration.',
'category': data_category,
'price': 4.50,
'form_schema': {
'fields': [
{
'name': 'csv_url',
'type': 'url',
'label': 'CSV File URL',
'placeholder': 'https://example.com/data.csv',
'required': True
},
{
'name': 'analysis_columns',
'type': 'text',
'label': 'Columns to Analyze (comma-separated)',
'placeholder': 'sales,revenue,date',
'required': False
},
{
'name': 'chart_type',
'type': 'select',
'label': 'Chart Type',
'options': [
{'value': 'line', 'label': 'Line Chart'},
{'value': 'bar', 'label': 'Bar Chart'},
{'value': 'pie', 'label': 'Pie Chart'},
{'value': 'scatter', 'label': 'Scatter Plot'}
],
'required': False
}
]
},
'webhook_url': 'https://your-n8n-instance.com/webhook/data-analyzer'
}
)
self.stdout.write(self.style.SUCCESS('Sample agents created successfully'))
self.stdout.write(f'Created categories: {AgentCategory.objects.count()}')
self.stdout.write(f'Created agents: {Agent.objects.count()}')
```
### agents/apps.py
```python
from django.apps import AppConfig
class AgentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'agents'
verbose_name = 'Agents'
```
## Setup Instructions
### 1. Run Migrations
```bash
python manage.py makemigrations agents
python manage.py migrate
```
### 2. Create Sample Data
```bash
python manage.py create_sample_agents
```
### 3. Create Superuser (if needed)
```bash
python manage.py createsuperuser
```
### 4. Test the API
Start the server and test these endpoints:
- `GET /api/agents/` - List all agents
- `GET /api/agents/pdf-analyzer/` - Agent details
- `POST /api/agents/execute/` - Execute an agent
- `GET /api/agents/executions/` - List executions
## API Usage Examples
### List Agents
```bash
curl -H "Authorization: Token YOUR_TOKEN" http://localhost:8000/api/agents/
```
### Execute Agent
```bash
curl -X POST \
-H "Authorization: Token YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_slug": "pdf-analyzer",
"input_data": {
"pdf_url": "https://example.com/document.pdf",
"analysis_type": "summary"
}
}' \
http://localhost:8000/api/agents/execute/
```
## Customization Notes
### User Model Integration
Update the `AgentExecution` model to reference your custom user model:
```python
# If your user model is in a different app
user = models.ForeignKey('accounts.CustomUser', on_delete=models.CASCADE)
```
### Wallet Integration
The code assumes your user model has these methods:
- `wallet_balance` property
- `deduct_balance(amount)` method
Adjust the wallet checking logic in `execute_agent` view as needed.
### n8n Webhook Format
The webhook payload sent to n8n includes:
- `execution_id`: UUID of the execution
- `agent_slug`: Identifier for the agent
- `user_id`: User who triggered the execution
- `input_data`: Form data submitted by user
## Features Included
**Agent Categories** - Organize agents by type
**Agent Management** - Full CRUD via Django admin
**Execution System** - Track agent runs with status
**Webhook Integration** - Connect to n8n workflows
**User Balance Checking** - Wallet integration ready
**REST API** - Complete API endpoints
**Pagination** - Built-in pagination for lists
**Error Handling** - Comprehensive error management
**Sample Data** - Management command for test data
**Form Schema** - Dynamic form generation support
**Admin Interface** - Django admin integration
**UUID Primary Keys** - Better security and uniqueness
## Production Considerations
1. **Environment Variables**: Store webhook URLs and API keys in environment variables
2. **Rate Limiting**: Add rate limiting to prevent abuse
3. **Caching**: Cache agent lists and categories for better performance
4. **Background Tasks**: Use Celery for long-running agent executions
5. **Logging**: Add comprehensive logging for debugging
6. **Monitoring**: Monitor webhook success rates and execution times
7. **Security**: Validate webhook responses and sanitize input data
This guide provides a complete, production-ready agents marketplace that can be easily integrated into any Django project.

0
agents/__init__.py Normal file
View File

24
agents/admin.py Normal file
View File

@ -0,0 +1,24 @@
from django.contrib import admin
from .models import AgentCategory, Agent, AgentExecution
@admin.register(AgentCategory)
class AgentCategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Agent)
class AgentAdmin(admin.ModelAdmin):
list_display = ['name', 'category', 'price', 'is_active', 'created_at']
list_filter = ['category', 'is_active', 'created_at']
search_fields = ['name', 'description', 'short_description']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['created_at', 'updated_at']
@admin.register(AgentExecution)
class AgentExecutionAdmin(admin.ModelAdmin):
list_display = ['agent', 'user', 'status', 'fee_charged', 'created_at']
list_filter = ['status', 'created_at', 'agent__category']
search_fields = ['agent__name', 'user__email']
readonly_fields = ['created_at', 'completed_at']

6
agents/apps.py Normal file
View File

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

View File

View File

View File

@ -0,0 +1,151 @@
from django.core.management.base import BaseCommand
from agents.models import AgentCategory, Agent
class Command(BaseCommand):
help = 'Create sample agents for testing'
def handle(self, *args, **options):
# Create categories
ai_category, _ = AgentCategory.objects.get_or_create(
slug='ai-tools',
defaults={
'name': 'AI Tools',
'description': 'AI-powered automation tools',
'icon': '🤖'
}
)
data_category, _ = AgentCategory.objects.get_or_create(
slug='data-analysis',
defaults={
'name': 'Data Analysis',
'description': 'Data processing and analysis tools',
'icon': '📊'
}
)
web_category, _ = AgentCategory.objects.get_or_create(
slug='web-scraping',
defaults={
'name': 'Web Scraping',
'description': 'Web data extraction tools',
'icon': '🕷️'
}
)
# Create sample agents
Agent.objects.get_or_create(
slug='pdf-analyzer',
defaults={
'name': 'PDF Content Analyzer',
'short_description': 'Extract and analyze content from PDF documents',
'description': 'This agent processes PDF files and extracts meaningful insights including summaries, keywords, and sentiment analysis. Perfect for document processing workflows.',
'category': ai_category,
'price': 5.00,
'form_schema': {
'fields': [
{
'name': 'pdf_url',
'type': 'url',
'label': 'PDF URL',
'placeholder': 'https://example.com/document.pdf',
'required': True
},
{
'name': 'analysis_type',
'type': 'select',
'label': 'Analysis Type',
'options': [
{'value': 'summary', 'label': 'Summary'},
{'value': 'keywords', 'label': 'Keywords'},
{'value': 'sentiment', 'label': 'Sentiment Analysis'}
],
'required': True
}
]
},
'webhook_url': 'https://your-n8n-instance.com/webhook/pdf-analyzer'
}
)
Agent.objects.get_or_create(
slug='website-scraper',
defaults={
'name': 'Website Data Scraper',
'short_description': 'Extract structured data from any website',
'description': 'Advanced web scraping agent that can extract specific data from websites using CSS selectors or XPath. Handles JavaScript-rendered content and returns clean, structured data.',
'category': web_category,
'price': 3.00,
'form_schema': {
'fields': [
{
'name': 'website_url',
'type': 'url',
'label': 'Website URL',
'placeholder': 'https://example.com',
'required': True
},
{
'name': 'selectors',
'type': 'textarea',
'label': 'CSS Selectors (one per line)',
'placeholder': 'h1.title\n.price\n.description',
'required': True
},
{
'name': 'wait_for_js',
'type': 'checkbox',
'label': 'Wait for JavaScript to load',
'required': False
}
]
},
'webhook_url': 'https://your-n8n-instance.com/webhook/website-scraper'
}
)
Agent.objects.get_or_create(
slug='data-analyzer',
defaults={
'name': 'CSV Data Analyzer',
'short_description': 'Analyze and visualize CSV data with insights',
'description': 'Upload CSV files and get comprehensive data analysis including statistics, trends, and visualizations. Perfect for business intelligence and data exploration.',
'category': data_category,
'price': 4.50,
'form_schema': {
'fields': [
{
'name': 'csv_url',
'type': 'url',
'label': 'CSV File URL',
'placeholder': 'https://example.com/data.csv',
'required': True
},
{
'name': 'analysis_columns',
'type': 'text',
'label': 'Columns to Analyze (comma-separated)',
'placeholder': 'sales,revenue,date',
'required': False
},
{
'name': 'chart_type',
'type': 'select',
'label': 'Chart Type',
'options': [
{'value': 'line', 'label': 'Line Chart'},
{'value': 'bar', 'label': 'Bar Chart'},
{'value': 'pie', 'label': 'Pie Chart'},
{'value': 'scatter', 'label': 'Scatter Plot'}
],
'required': False
}
]
},
'webhook_url': 'https://your-n8n-instance.com/webhook/data-analyzer'
}
)
self.stdout.write(self.style.SUCCESS('Sample agents created successfully'))
self.stdout.write(f'Created categories: {AgentCategory.objects.count()}')
self.stdout.write(f'Created agents: {Agent.objects.count()}')

View File

@ -0,0 +1,101 @@
from django.core.management.base import BaseCommand
from agents.models import AgentCategory, Agent
class Command(BaseCommand):
help = 'Create social ads agent for testing'
def handle(self, *args, **options):
# Create Marketing category
marketing_category, created = AgentCategory.objects.get_or_create(
slug='marketing',
defaults={
'name': 'Marketing & Advertising',
'description': 'AI-powered marketing and advertising tools',
'icon': '📢'
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created category: {marketing_category.name}'))
else:
self.stdout.write(f'Category already exists: {marketing_category.name}')
# Create Social Ads Generator agent
social_ads_agent, created = Agent.objects.get_or_create(
slug='social-ads-generator',
defaults={
'name': 'Social Ads Generator',
'short_description': 'Create compelling social media advertisements optimized for different platforms',
'description': 'Generate engaging social media advertisements with AI-powered content generation. Optimized for Facebook, Instagram, LinkedIn, Twitter, TikTok, and YouTube. Includes platform-specific formatting, emoji support, and multi-language capabilities.',
'category': marketing_category,
'price': 6.0,
'form_schema': {
'fields': [
{
'name': 'description',
'type': 'textarea',
'label': 'Describe what you\'d like to generate',
'placeholder': 'Describe the product, service, or campaign you want to create an ad for. Include key features, target audience, and any specific messaging you want to emphasize.',
'required': True,
'rows': 4,
'help_text': 'Provide clear, specific information about your product or service for better ad copy'
},
{
'name': 'social_platform',
'type': 'select',
'label': 'For Social Media Platform',
'required': True,
'options': [
{'value': '', 'label': 'Select a platform...'},
{'value': 'facebook', 'label': 'Facebook'},
{'value': 'instagram', 'label': 'Instagram'},
{'value': 'linkedin', 'label': 'LinkedIn'},
{'value': 'twitter', 'label': 'X (Twitter)'},
{'value': 'tiktok', 'label': 'TikTok'},
{'value': 'youtube', 'label': 'YouTube'}
],
'help_text': 'Choose the social media platform for optimization'
},
{
'name': 'include_emoji',
'type': 'select',
'label': 'Include Emoji',
'required': True,
'options': [
{'value': '', 'label': 'Select an option...'},
{'value': 'yes', 'label': 'Yes'},
{'value': 'no', 'label': 'No'}
],
'help_text': 'Whether to include emojis in the ad copy'
},
{
'name': 'language',
'type': 'select',
'label': 'Language',
'required': False,
'default': 'English',
'options': [
{'value': 'English', 'label': 'English'},
{'value': 'Arabic', 'label': 'Arabic (العربية)'},
{'value': 'Spanish', 'label': 'Spanish (Español)'},
{'value': 'French', 'label': 'French (Français)'},
{'value': 'German', 'label': 'German (Deutsch)'},
{'value': 'Chinese', 'label': 'Chinese (中文)'}
],
'help_text': 'Select the primary language for the ad copy'
}
]
},
'webhook_url': 'http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d'
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created agent: {social_ads_agent.name}'))
else:
self.stdout.write(f'Agent already exists: {social_ads_agent.name}')
self.stdout.write(self.style.SUCCESS('Social Ads Agent setup completed successfully'))
self.stdout.write(f'Agent ID: {social_ads_agent.id}')
self.stdout.write(f'Agent Slug: {social_ads_agent.slug}')
self.stdout.write(f'Price: {social_ads_agent.price} AED')

View File

@ -0,0 +1,140 @@
# Generated by Django 5.2.4 on 2025-07-31 04:22
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="AgentCategory",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=100)),
("slug", models.SlugField(unique=True)),
("description", models.TextField(blank=True)),
(
"icon",
models.CharField(
blank=True, help_text="Icon class or emoji", max_length=50
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="Agent",
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)),
("short_description", models.CharField(max_length=300)),
("description", models.TextField()),
("price", models.DecimalField(decimal_places=2, max_digits=10)),
(
"form_schema",
models.JSONField(help_text="JSON schema for agent input form"),
),
(
"webhook_url",
models.URLField(help_text="n8n webhook URL for execution"),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="agents",
to="agents.agentcategory",
),
),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="AgentExecution",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("input_data", models.JSONField()),
("output_data", models.JSONField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("running", "Running"),
("completed", "Completed"),
("failed", "Failed"),
],
default="pending",
max_length=20,
),
),
("fee_charged", models.DecimalField(decimal_places=2, max_digits=10)),
("webhook_response", models.JSONField(blank=True, null=True)),
("error_message", models.TextField(blank=True)),
("execution_time", models.DurationField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("completed_at", models.DateTimeField(blank=True, null=True)),
(
"agent",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="executions",
to="agents.agent",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

64
agents/models.py Normal file
View File

@ -0,0 +1,64 @@
from django.db import models
import uuid
class AgentCategory(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
icon = models.CharField(max_length=50, blank=True, help_text="Icon class or emoji")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Agent(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
short_description = models.CharField(max_length=300)
description = models.TextField()
category = models.ForeignKey(AgentCategory, on_delete=models.CASCADE, related_name='agents')
price = models.DecimalField(max_digits=10, decimal_places=2)
form_schema = models.JSONField(help_text="JSON schema for agent input form")
webhook_url = models.URLField(help_text="n8n webhook URL for execution")
is_active = models.BooleanField(default=True)
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
class AgentExecution(models.Model):
STATUS_CHOICES = [
('pending', 'Pending'),
('running', 'Running'),
('completed', 'Completed'),
('failed', 'Failed'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name='executions')
user = models.ForeignKey('authentication.User', on_delete=models.CASCADE)
input_data = models.JSONField()
output_data = models.JSONField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
fee_charged = models.DecimalField(max_digits=10, decimal_places=2)
webhook_response = models.JSONField(null=True, blank=True)
error_message = models.TextField(blank=True)
execution_time = models.DurationField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.agent.name} - {self.user.email} - {self.status}"

28
agents/serializers.py Normal file
View File

@ -0,0 +1,28 @@
from rest_framework import serializers
from .models import Agent, AgentCategory, AgentExecution
class AgentCategorySerializer(serializers.ModelSerializer):
class Meta:
model = AgentCategory
fields = ['id', 'name', 'slug', 'description', 'icon']
class AgentSerializer(serializers.ModelSerializer):
category = AgentCategorySerializer(read_only=True)
class Meta:
model = Agent
fields = [
'id', 'name', 'slug', 'short_description', 'description',
'category', 'price', 'form_schema', 'created_at'
]
class AgentExecutionSerializer(serializers.ModelSerializer):
agent = AgentSerializer(read_only=True)
class Meta:
model = AgentExecution
fields = [
'id', 'agent', 'input_data', 'output_data', 'status',
'fee_charged', 'error_message', 'execution_time',
'created_at', 'completed_at'
]

View File

@ -0,0 +1,368 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ agent.name }} - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<style>
/* Enhanced Agent Template Styles */
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
font-size: 14px;
line-height: 1.5;
transition: all 0.2s ease;
background: var(--surface);
color: var(--on-surface);
font-family: inherit;
resize: vertical;
min-height: 120px;
}
.form-textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.form-textarea:hover {
border-color: var(--on-surface-variant);
}
/* Enhanced Form Sections */
.section-container {
margin-bottom: var(--spacing-xl);
padding: var(--spacing-lg);
background: var(--surface-variant);
border-radius: var(--radius-md);
border: 1px solid var(--outline-variant);
}
.section-subtitle {
font-size: 16px;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 var(--spacing-lg) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.section-subtitle::before {
content: '';
width: 3px;
height: 16px;
background: var(--primary);
border-radius: 2px;
}
/* Error styling */
.form-textarea.error,
.form-input.error {
border-color: var(--error);
}
.form-error {
color: var(--error);
font-size: 12px;
margin-top: var(--spacing-xs);
font-weight: 500;
}
/* Enhanced Results Display */
.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;
}
/* 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(--spacing-sm);
border-left: 4px solid var(--primary);
margin: var(--spacing-lg) 0 var(--spacing-md) 0;
}
.results-content strong {
color: var(--primary);
font-weight: 600;
}
/* 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;
}
.toast.info {
border-color: var(--primary);
background: #f0f9ff;
color: #0369a1;
}
/* Responsive Design */
@media (max-width: 768px) {
.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);
}
.section-container {
padding: var(--spacing-md);
}
}
</style>
{% endblock %}
{% block content %}
<script>
// Set data attributes for JavaScript access
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
document.body.setAttribute('data-agent-price', '{{ agent.price }}');
document.body.setAttribute('data-agent-id', '{{ agent.id }}');
document.body.setAttribute('data-agent-slug', '{{ agent.slug }}');
document.body.setAttribute('data-webhook-url', '{{ agent.webhook_url }}');
{% if user.is_authenticated %}
document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
{% endif %}
</script>
<div class="agent-container">
<!-- Agent Header Component -->
{% include "workflows/components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %}
<!-- Quick Agent Access Panel Component -->
{% include "workflows/components/quick_agents_panel.html" %}
<!-- Main Agent Grid -->
<div class="agent-grid">
<!-- Agent 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">{{ agent.category.icon }}</span>
{{ agent.name }} Details
</h3>
</div>
<div class="widget-content">
<form id="agentForm" method="POST" data-agent-id="{{ agent.id }}">
{% csrf_token %}
<!-- Dynamic Form Fields -->
<div class="section-container">
<h4 class="section-subtitle">{{ agent.category.icon }} {{ agent.name }} Configuration</h4>
{% for field in agent.form_schema.fields %}
<div class="form-group" data-field-name="{{ field.name }}">
<label class="form-label" for="{{ field.name }}">
{{ field.label }}{% if field.required %} *{% endif %}
</label>
{% if field.type == 'textarea' %}
<textarea
id="{{ field.name }}"
name="{{ field.name }}"
class="form-textarea"
placeholder="{{ field.placeholder|default:'' }}"
{% if field.required %}required{% endif %}
{% if field.rows %}rows="{{ field.rows }}"{% endif %}
>{{ field.default|default:'' }}</textarea>
{% elif field.type == 'select' %}
<select
id="{{ field.name }}"
name="{{ field.name }}"
class="form-input"
{% if field.required %}required{% endif %}
>
{% for option in field.options %}
<option value="{{ option.value }}"
{% if option.value == field.default %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
{% elif field.type == 'text' %}
<input
type="text"
id="{{ field.name }}"
name="{{ field.name }}"
class="form-input"
placeholder="{{ field.placeholder|default:'' }}"
value="{{ field.default|default:'' }}"
{% if field.required %}required{% endif %}
/>
{% elif field.type == 'url' %}
<input
type="url"
id="{{ field.name }}"
name="{{ field.name }}"
class="form-input"
placeholder="{{ field.placeholder|default:'' }}"
value="{{ field.default|default:'' }}"
{% if field.required %}required{% endif %}
/>
{% elif field.type == 'checkbox' %}
<label class="checkbox-container">
<input
type="checkbox"
id="{{ field.name }}"
name="{{ field.name }}"
value="true"
{% if field.default %}checked{% endif %}
/>
<span class="checkmark"></span>
{{ field.label }}
</label>
{% endif %}
{% if field.help_text %}
<div class="form-help">{{ field.help_text }}</div>
{% endif %}
<div id="{{ field.name }}-error" class="form-error" style="display: none;"></div>
</div>
{% endfor %}
</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="generateBtn">
{{ agent.category.icon }} Execute {{ agent.name }} ({{ 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 -->
{% include "workflows/components/how_it_works_widget.html" with steps="agents" %}
</div>
<!-- Processing Status Component -->
{% include "workflows/components/processing_status.html" with status_title="Processing..." status_text="Please wait while we execute your agent..." %}
<!-- Results Component -->
{% include "workflows/components/results_container.html" with results_title="Agent Results" %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
<script src="{% static 'js/agents-core.js' %}?v={{ timestamp }}"></script>
{% endblock %}

View File

@ -0,0 +1,348 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}AI Agents Marketplace - Quantum Tasks AI{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
<style>
/* Marketplace Specific Styles */
.marketplace-header {
text-align: center;
margin-bottom: var(--spacing-xl);
padding: var(--spacing-xl) 0;
}
.marketplace-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--on-surface);
margin-bottom: var(--spacing-md);
}
.marketplace-subtitle {
font-size: 1.1rem;
color: var(--on-surface-variant);
max-width: 600px;
margin: 0 auto var(--spacing-xl) auto;
}
.marketplace-filters {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.search-box {
flex: 1;
max-width: 400px;
position: relative;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 44px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
font-size: 14px;
background: var(--surface);
color: var(--on-surface);
}
.search-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--on-surface-variant);
}
.category-filter {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.category-select {
padding: 8px 16px;
border: 2px solid var(--outline-variant);
border-radius: var(--radius-md);
background: var(--surface);
color: var(--on-surface);
font-size: 14px;
min-width: 160px;
}
.results-info {
text-align: center;
margin-bottom: var(--spacing-lg);
color: var(--on-surface-variant);
font-size: 14px;
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.agent-card {
background: var(--surface);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
.agent-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--primary);
}
.agent-card-header {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.agent-icon {
font-size: 2rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-variant);
border-radius: var(--radius-md);
flex-shrink: 0;
}
.agent-meta {
flex: 1;
min-width: 0;
}
.agent-name {
font-size: 1.1rem;
font-weight: 600;
color: var(--on-surface);
margin: 0 0 4px 0;
word-wrap: break-word;
}
.agent-category {
font-size: 12px;
color: var(--on-surface-variant);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.agent-price {
font-size: 1rem;
font-weight: 600;
color: var(--primary);
flex-shrink: 0;
}
.agent-description {
color: var(--on-surface-variant);
font-size: 14px;
line-height: 1.4;
margin-bottom: var(--spacing-lg);
}
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: var(--spacing-xl);
color: var(--on-surface-variant);
}
.no-results-icon {
font-size: 4rem;
margin-bottom: var(--spacing-md);
}
.categories-section {
margin-top: var(--spacing-xl);
padding-top: var(--spacing-xl);
border-top: 1px solid var(--outline-variant);
}
.categories-section h2 {
text-align: center;
margin-bottom: var(--spacing-xl);
color: var(--on-surface);
}
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
}
.category-card {
background: var(--surface);
border: 1px solid var(--outline-variant);
border-radius: var(--radius-md);
padding: var(--spacing-lg);
text-decoration: none;
transition: all 0.2s ease;
display: block;
}
.category-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--primary);
}
.category-icon {
font-size: 2rem;
margin-bottom: var(--spacing-sm);
}
.category-card h3 {
color: var(--on-surface);
margin: 0 0 var(--spacing-sm) 0;
font-size: 1.1rem;
}
.category-card p {
color: var(--on-surface-variant);
font-size: 14px;
margin: 0 0 var(--spacing-sm) 0;
}
.category-count {
color: var(--primary);
font-size: 12px;
font-weight: 600;
}
/* Responsive Design */
@media (max-width: 768px) {
.marketplace-title {
font-size: 2rem;
}
.marketplace-filters {
flex-direction: column;
align-items: stretch;
}
.search-box {
max-width: none;
}
.agents-grid {
grid-template-columns: 1fr;
}
.categories-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="agent-container">
<!-- Marketplace Header -->
<div class="marketplace-header">
<h1 class="marketplace-title">🤖 AI Agents Marketplace</h1>
<p class="marketplace-subtitle">Discover powerful AI agents to automate your tasks and boost productivity</p>
<!-- Search and Filters -->
<div class="marketplace-filters">
<form method="GET" style="display: flex; gap: var(--spacing-md); flex-wrap: wrap; justify-content: center; width: 100%;">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" name="search" value="{{ search_query }}"
placeholder="Search agents..." class="search-input">
</div>
<div class="category-filter">
<select name="category" onchange="this.form.submit()" class="category-select">
<option value="">All Categories</option>
{% for category in categories %}
<option value="{{ category.slug }}"
{% if category.slug == selected_category %}selected{% endif %}>
{{ category.icon }} {{ category.name }}
</option>
{% endfor %}
</select>
</div>
</form>
</div>
</div>
<!-- Results Info -->
<div class="results-info">
<p>{{ agents.count }} agent{{ agents.count|pluralize }} found</p>
</div>
<!-- Agents Grid -->
<div class="agents-grid">
{% for agent in agents %}
<div class="agent-card">
<div class="agent-card-header">
<div class="agent-icon">{{ agent.category.icon }}</div>
<div class="agent-meta">
<h3 class="agent-name">{{ agent.name }}</h3>
<span class="agent-category">{{ agent.category.name }}</span>
</div>
<div class="agent-price">{{ agent.price }} AED</div>
</div>
<div class="agent-card-body">
<p class="agent-description">{{ agent.short_description }}</p>
</div>
<div class="agent-card-footer">
<a href="{% url 'agents:detail' agent.slug %}" class="btn btn-primary btn-full">
🚀 Use Agent
</a>
</div>
</div>
{% empty %}
<div class="no-results">
<div class="no-results-icon">🤖</div>
<h3>No agents found</h3>
<p>Try adjusting your search or filter criteria</p>
<a href="{% url 'agents:marketplace' %}" class="btn btn-primary">View All Agents</a>
</div>
{% endfor %}
</div>
<!-- Categories Overview -->
{% if not selected_category and not search_query %}
<div class="categories-section">
<h2>Browse by Category</h2>
<div class="categories-grid">
{% for category in categories %}
<a href="?category={{ category.slug }}" class="category-card">
<div class="category-icon">{{ category.icon }}</div>
<h3>{{ category.name }}</h3>
<p>{{ category.description }}</p>
<span class="category-count">{{ category.agents.count }} agent{{ category.agents.count|pluralize }}</span>
</a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

3
agents/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

19
agents/urls.py Normal file
View File

@ -0,0 +1,19 @@
from django.urls import path
from . import views
app_name = 'agents'
urlpatterns = [
# Web interface
path('', views.agents_marketplace, name='marketplace'),
# API endpoints - specific URLs first to avoid slug conflicts
path('api/execute/', views.execute_agent, name='execute_agent'),
path('api/executions/', views.execution_list, name='execution_list'),
path('api/executions/<uuid:execution_id>/', views.execution_detail, name='execution_detail'),
path('api/', views.agent_list, name='agent_list'),
path('api/<slug:slug>/', views.agent_detail, name='agent_detail_api'),
# Agent detail page (must be last to avoid conflicts)
path('<slug:slug>/', views.agent_detail_view, name='detail'),
]

214
agents/views.py Normal file
View File

@ -0,0 +1,214 @@
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django.db import models
from .models import Agent, AgentExecution, AgentCategory
from .serializers import AgentSerializer, AgentExecutionSerializer
import requests
import json
import time
import uuid
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def agent_list(request):
"""List all active agents with optional category filtering"""
agents = Agent.objects.filter(is_active=True)
category = request.GET.get('category')
if category:
agents = agents.filter(category__slug=category)
search = request.GET.get('search')
if search:
agents = agents.filter(name__icontains=search)
paginator = PageNumberPagination()
paginator.page_size = 20
result_page = paginator.paginate_queryset(agents, request)
serializer = AgentSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def agent_detail(request, slug):
"""Get detailed agent information"""
agent = get_object_or_404(Agent, slug=slug, is_active=True)
serializer = AgentSerializer(agent)
return Response(serializer.data)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def execute_agent(request):
"""Execute an agent with provided input data"""
agent_slug = request.data.get('agent_slug')
input_data = request.data.get('input_data', {})
if not agent_slug:
return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
agent = get_object_or_404(Agent, slug=agent_slug, is_active=True)
# Check if user has sufficient balance (using existing wallet system)
if hasattr(request.user, 'has_sufficient_balance') and not request.user.has_sufficient_balance(agent.price):
return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
# Create execution record
execution = AgentExecution.objects.create(
agent=agent,
user=request.user,
input_data=input_data,
fee_charged=agent.price,
status='pending'
)
try:
# Deduct fee from user wallet (using existing wallet system)
if hasattr(request.user, 'deduct_balance'):
success = request.user.deduct_balance(
agent.price,
f'{agent.name} - Execution {str(execution.id)[:8]}',
agent.slug
)
if not success:
execution.status = 'failed'
execution.error_message = 'Failed to deduct wallet balance'
execution.save()
return Response({'error': 'Failed to deduct wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
# Call n8n webhook with proper payload format
execution.status = 'running'
execution.save()
# Generate session ID
session_id = f"session_{int(time.time() * 1000)}_{str(uuid.uuid4())[:8]}"
# Format message text for N8N based on agent type
message_text = format_agent_message(agent.slug, input_data)
webhook_payload = {
'sessionId': session_id,
'message': {'text': message_text},
'webhookUrl': agent.webhook_url,
'executionMode': 'production',
'agentId': str(agent.id),
'executionId': str(execution.id),
'userId': str(request.user.id)
}
response = requests.post(
agent.webhook_url,
json=webhook_payload,
timeout=90, # Increased timeout for complex processing
headers={'Content-Type': 'application/json'}
)
# Store webhook response
execution.webhook_response = response.json() if response.headers.get('content-type', '').startswith('application/json') else {'raw': response.text}
if response.status_code == 200:
execution.status = 'completed'
execution.output_data = execution.webhook_response
else:
execution.status = 'failed'
execution.error_message = f"Webhook returned {response.status_code}: {response.text[:500]}"
execution.completed_at = timezone.now()
execution.save()
serializer = AgentExecutionSerializer(execution)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except requests.RequestException as e:
execution.status = 'failed'
execution.error_message = str(e)
execution.completed_at = timezone.now()
execution.save()
return Response({
'error': 'Failed to execute agent',
'execution_id': str(execution.id)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def execution_list(request):
"""List user's agent executions"""
executions = AgentExecution.objects.filter(user=request.user)
paginator = PageNumberPagination()
paginator.page_size = 20
result_page = paginator.paginate_queryset(executions, request)
serializer = AgentExecutionSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def execution_detail(request, execution_id):
"""Get detailed execution information"""
execution = get_object_or_404(AgentExecution, id=execution_id, user=request.user)
serializer = AgentExecutionSerializer(execution)
return Response(serializer.data)
def format_agent_message(agent_slug, input_data):
"""Format input data into a message for N8N webhook based on agent type"""
if agent_slug == 'social-ads-generator':
description = input_data.get('description', '')
platform = input_data.get('social_platform', '')
emoji = input_data.get('include_emoji', 'yes')
language = input_data.get('language', 'English')
return f"Execute Social Media Ad Creator with the following parameters:. Describe what you'd like to generate: {description}. Include Emoji: {emoji.title()}. For Social Media Platform: {platform.title()}. Language: {language}."
# Default formatting for other agents
params = [f"{key}: {value}" for key, value in input_data.items() if value]
return f"Execute {agent_slug.replace('-', ' ').title()} with parameters: {'. '.join(params)}."
# Web interface views
def agent_detail_view(request, slug):
"""Render agent detail page with dynamic form"""
agent = get_object_or_404(Agent, slug=slug, is_active=True)
context = {
'agent': agent,
'timestamp': int(time.time()) # For cache busting
}
return render(request, 'agents/agent_detail.html', context)
def agents_marketplace(request):
"""Agent marketplace view"""
agents = Agent.objects.filter(is_active=True).select_related('category')
categories = AgentCategory.objects.filter(is_active=True)
# Filter by category
category_slug = request.GET.get('category')
if category_slug:
agents = agents.filter(category__slug=category_slug)
# Search functionality
search_query = request.GET.get('search', '').strip()
if search_query:
agents = agents.filter(
models.Q(name__icontains=search_query) |
models.Q(short_description__icontains=search_query) |
models.Q(description__icontains=search_query)
)
context = {
'agents': agents,
'categories': categories,
'selected_category': category_slug,
'search_query': search_query,
'timestamp': int(time.time())
}
return render(request, 'agents/marketplace.html', context)

View File

@ -76,6 +76,7 @@ INSTALLED_APPS = [
'wallet', 'wallet',
'core', 'core',
'workflows', # Unified workflows app (includes marketplace and agent execution) 'workflows', # Unified workflows app (includes marketplace and agent execution)
'agents', # New REST API-based agents system
] ]
# Development apps (only in DEBUG mode) # Development apps (only in DEBUG mode)

View File

@ -25,7 +25,10 @@ urlpatterns = [
path('wallet/', include('wallet.urls')), path('wallet/', include('wallet.urls')),
# Unified workflows system for all agents (includes marketplace) # Unified workflows system for all agents (includes marketplace)
path('agents/', include('workflows.urls')), path('workflows/', include('workflows.urls')),
# New REST API-based agents system (web interface + API)
path('agents/', include('agents.urls')),
path('', include('core.urls')), path('', include('core.urls')),
] ]

397
static/js/agents-core.js Normal file
View File

@ -0,0 +1,397 @@
/**
* Agents Core - Dynamic Agent Execution System
* Handles form submission and N8N integration for any agent
*/
class AgentsCore extends WorkflowsCore {
constructor() {
super();
this.agentId = document.body.getAttribute('data-agent-id');
this.agentSlug = document.body.getAttribute('data-agent-slug');
this.webhookUrl = document.body.getAttribute('data-webhook-url');
this.price = parseFloat(document.body.getAttribute('data-agent-price') || '0');
this.sessionId = this.constructor.generateSessionId();
// Initialize on page load
this.initialize();
}
initialize() {
// Initialize form submission
const form = document.getElementById('agentForm');
if (form) {
form.addEventListener('submit', this.handleFormSubmission.bind(this));
}
// Initialize form validation
this.initializeDynamicFormValidation();
}
/**
* Handle form submission with agents API integration
*/
async handleFormSubmission(e) {
e.preventDefault();
if (!this.isFormValid()) {
this.constructor.showToast('Please fill in all required fields correctly', 'error');
return;
}
// Check authentication and balance
if (!this.constructor.checkAuthentication()) return;
if (!this.constructor.checkBalance(this.price)) return;
// Show processing status and disable submit button
this.constructor.showProcessing('Executing agent...');
const submitBtn = document.getElementById('generateBtn');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Processing...';
}
try {
// Use agents API for execution
await this.executeViaAgentsAPI(e.target);
} catch (error) {
console.error('Form submission error:', error);
this.constructor.hideProcessing();
this.constructor.showToast('❌ Connection error. Please try again.', 'error');
this.resetSubmitButton();
}
}
/**
* Execute agent via the agents API
*/
async executeViaAgentsAPI(form) {
try {
const formData = new FormData(form);
// Extract all form data dynamically
const inputData = {};
for (let [key, value] of formData.entries()) {
if (key !== 'csrfmiddlewaretoken') {
inputData[key] = value;
}
}
// Get CSRF token
const csrfToken = formData.get('csrfmiddlewaretoken');
// Call agents API
const response = await fetch('/agents/api/execute/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
agent_slug: this.agentSlug,
input_data: inputData
}),
signal: AbortSignal.timeout(90000) // 90 second timeout
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `API error: ${response.status}`);
}
const data = await response.json();
// Process successful execution
this.constructor.hideProcessing();
// Update wallet balance if fee was charged
if (data.fee_charged) {
const currentBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');
const newBalance = currentBalance - parseFloat(data.fee_charged);
// Update the wallet balance display
this.constructor.updateWalletBalance(newBalance);
// Update the data attribute for future calculations
document.body.setAttribute('data-user-balance', newBalance.toString());
}
// Display results
this.displayExecutionResults(data);
this.constructor.showToast('✅ Agent executed successfully!', 'success');
} catch (error) {
console.error('Agent execution error:', error);
this.constructor.hideProcessing();
this.constructor.showToast(`${error.message}`, 'error');
this.resetSubmitButton();
}
}
/**
* Display results from agent execution
*/
displayExecutionResults(executionData) {
const resultsContainer = document.getElementById('resultsContainer');
const resultsContent = document.getElementById('resultsContent');
if (!resultsContainer || !resultsContent) return;
let content = '';
// Handle different response formats
if (executionData.output_data && typeof executionData.output_data === 'object') {
// Handle N8N response formats
const output = executionData.output_data;
content = output.output || output.text || output.content || output.result || output.message || JSON.stringify(output, null, 2);
} else if (executionData.output_data && typeof executionData.output_data === 'string') {
content = executionData.output_data;
} else {
content = `Agent executed successfully!\n\nExecution ID: ${executionData.id}\nStatus: ${executionData.status}\nFee Charged: ${executionData.fee_charged} AED`;
}
// Clear and populate results securely
resultsContent.textContent = '';
this.renderSecureContent(resultsContent, content);
// Show results container
resultsContainer.style.display = 'block';
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
this.resetSubmitButton();
}
/**
* Secure content rendering without innerHTML to prevent XSS
*/
renderSecureContent(container, content) {
// Sanitize and validate content
if (!content || typeof content !== 'string') {
container.textContent = 'No content available';
return;
}
// Create wrapper div
const wrapper = document.createElement('div');
wrapper.className = 'results-content';
// Split content into lines and process safely
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) {
// Add line break for empty lines
if (i > 0) wrapper.appendChild(document.createElement('br'));
continue;
}
let element;
// Handle headers (but escape content)
if (line.startsWith('### ')) {
element = document.createElement('h3');
element.textContent = line.substring(4);
} else if (line.startsWith('## ')) {
element = document.createElement('h2');
element.textContent = line.substring(3);
} else if (line.startsWith('# ')) {
element = document.createElement('h1');
element.textContent = line.substring(2);
} else {
// Handle regular text with basic formatting
element = document.createElement('span');
this.formatTextSecurely(element, line);
}
wrapper.appendChild(element);
// Add line break if not the last line
if (i < lines.length - 1) {
wrapper.appendChild(document.createElement('br'));
}
}
container.appendChild(wrapper);
}
/**
* Format text with basic styling while preventing XSS
*/
formatTextSecurely(element, text) {
// Simple approach: handle bold and italic formatting securely
const parts = [];
let currentText = text;
// Process **bold** text
currentText = currentText.replace(/\*\*(.*?)\*\*/g, (match, content) => {
const placeholder = `__BOLD_${parts.length}__`;
parts.push({type: 'bold', content: content});
return placeholder;
});
// Process *italic* text
currentText = currentText.replace(/\*(.*?)\*/g, (match, content) => {
const placeholder = `__ITALIC_${parts.length}__`;
parts.push({type: 'italic', content: content});
return placeholder;
});
// Split by placeholders and create DOM elements
const segments = currentText.split(/(__(?:BOLD|ITALIC)_\d+__)/);
segments.forEach(segment => {
if (segment.startsWith('__BOLD_')) {
const index = parseInt(segment.match(/\d+/)[0]);
const strong = document.createElement('strong');
strong.textContent = parts[index].content;
element.appendChild(strong);
} else if (segment.startsWith('__ITALIC_')) {
const index = parseInt(segment.match(/\d+/)[0]);
const em = document.createElement('em');
em.textContent = parts[index].content;
element.appendChild(em);
} else if (segment) {
element.appendChild(document.createTextNode(segment));
}
});
}
/**
* Initialize dynamic form validation based on form schema
*/
initializeDynamicFormValidation() {
const fields = document.querySelectorAll('#agentForm [name]');
fields.forEach(field => {
const fieldName = field.getAttribute('name');
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
field.addEventListener('blur', () => this.validateField(fieldName));
field.addEventListener('input', () => this.constructor.clearFieldError(fieldName));
}
});
}
validateField(fieldName) {
const field = document.getElementById(fieldName);
if (!field) return true;
const value = field.type === 'checkbox' ? field.checked : field.value.trim();
const required = field.hasAttribute('required');
// Basic required field validation
if (required && (!value || value === '')) {
this.constructor.showFieldError(fieldName, `${fieldName.replace('_', ' ')} is required`);
return false;
}
// Specific validation based on field type
if (field.type === 'textarea' && value && value.length < 10) {
this.constructor.showFieldError(fieldName, 'Please provide more detailed information (at least 10 characters)');
return false;
}
if (field.type === 'url' && value && !this.isValidURL(value)) {
this.constructor.showFieldError(fieldName, 'Please enter a valid URL');
return false;
}
this.constructor.clearFieldError(fieldName);
return true;
}
isValidURL(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
isFormValid() {
const fields = document.querySelectorAll('#agentForm [name]');
let isValid = true;
fields.forEach(field => {
const fieldName = field.getAttribute('name');
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
if (!this.validateField(fieldName)) {
isValid = false;
}
}
});
return isValid;
}
/**
* Reset submit button to original state
*/
resetSubmitButton() {
const submitBtn = document.getElementById('generateBtn');
if (submitBtn) {
submitBtn.disabled = false;
const agentSlug = document.body.getAttribute('data-agent-slug') || 'agent';
const agentName = agentSlug.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
submitBtn.textContent = `🚀 Execute ${agentName} (${this.price} AED)`;
}
}
}
// Result action functions (global for button onclick handlers)
function copyResults() {
const content = document.getElementById('resultsContent');
if (content) {
const text = content.textContent || '';
WorkflowsCore.copyToClipboard(text, 'Results copied to clipboard!');
}
}
function downloadResults() {
const content = document.getElementById('resultsContent');
if (content) {
const text = content.textContent || '';
const agentSlug = document.body.getAttribute('data-agent-slug') || 'agent';
WorkflowsCore.downloadAsFile(text, `${agentSlug}-results.txt`, 'Results downloaded!');
}
}
function resetForm() {
const form = document.getElementById('agentForm');
if (form) {
// Reset form but preserve CSRF token
const csrfToken = form.querySelector('[name="csrfmiddlewaretoken"]').value;
form.reset();
form.querySelector('[name="csrfmiddlewaretoken"]').value = csrfToken;
}
const resultsContainer = document.getElementById('resultsContainer');
const processingStatus = document.getElementById('processingStatus');
if (resultsContainer) resultsContainer.style.display = 'none';
if (processingStatus) processingStatus.style.display = 'none';
// Clear validation errors
const fields = document.querySelectorAll('#agentForm [name]');
fields.forEach(field => {
const fieldName = field.getAttribute('name');
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
WorkflowsCore.clearFieldError(fieldName);
}
});
// Scroll back to form
const formSection = document.getElementById('agentForm');
if (formSection) {
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// Initialize Agents Core when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize processor (data attributes set by template)
window.agentsCore = new AgentsCore();
});

View File

@ -27,6 +27,7 @@ AGENT_CONFIGS = {
'icon': '📊', 'icon': '📊',
'webhook_url': 'http://localhost:5678/webhook/simple-pdf-processor', 'webhook_url': 'http://localhost:5678/webhook/simple-pdf-processor',
}, },
} }

View File

@ -258,16 +258,21 @@ def process_workflow_request(request, agent_slug, agent_config, agent):
status='processing' status='processing'
) )
# Actually send request to webhook (especially for data-analyzer) # Actually send request to webhook (for agents that support file upload)
logger.info(f"🔍 DEBUG: Checking webhook conditions - agent_slug={agent_slug}, has_file={'file' in uploaded_files}") logger.info(f"🔍 DEBUG: Checking webhook conditions - agent_slug={agent_slug}, has_file={'document_file' in uploaded_files or 'file' in uploaded_files}")
if agent_slug == 'data-analyzer' and 'file' in uploaded_files: file_upload_agents = ['data-analyzer']
has_file = 'file' in uploaded_files or 'document_file' in uploaded_files
if agent_slug in file_upload_agents and has_file:
try: try:
# Check if this is an AJAX request (like the original system) # Check if this is an AJAX request (like the original system)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Send file to webhook in background and return JSON response # Send file to webhook in background and return JSON response
# Get the uploaded file (different field names for different agents)
uploaded_file = uploaded_files.get('file') or uploaded_files.get('document_file')
webhook_result = send_file_to_webhook( webhook_result = send_file_to_webhook(
agent_config['webhook_url'], agent_config['webhook_url'],
uploaded_files['file'], uploaded_file,
form_data form_data
) )