mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 10:12:58 +00:00
🚀 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:
parent
8097f6f4c7
commit
5eba8fee84
639
AGENTS_APP_RECREATION_GUIDE.md
Normal file
639
AGENTS_APP_RECREATION_GUIDE.md
Normal 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
0
agents/__init__.py
Normal file
24
agents/admin.py
Normal file
24
agents/admin.py
Normal 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
6
agents/apps.py
Normal 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'
|
||||||
0
agents/management/__init__.py
Normal file
0
agents/management/__init__.py
Normal file
0
agents/management/commands/__init__.py
Normal file
0
agents/management/commands/__init__.py
Normal file
151
agents/management/commands/create_sample_agents.py
Normal file
151
agents/management/commands/create_sample_agents.py
Normal 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()}')
|
||||||
101
agents/management/commands/create_social_ads_agent.py
Normal file
101
agents/management/commands/create_social_ads_agent.py
Normal 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')
|
||||||
140
agents/migrations/0001_initial.py
Normal file
140
agents/migrations/0001_initial.py
Normal 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"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
agents/migrations/__init__.py
Normal file
0
agents/migrations/__init__.py
Normal file
64
agents/models.py
Normal file
64
agents/models.py
Normal 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
28
agents/serializers.py
Normal 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'
|
||||||
|
]
|
||||||
368
agents/templates/agents/agent_detail.html
Normal file
368
agents/templates/agents/agent_detail.html
Normal 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 %}
|
||||||
348
agents/templates/agents/marketplace.html
Normal file
348
agents/templates/agents/marketplace.html
Normal 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
3
agents/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
19
agents/urls.py
Normal file
19
agents/urls.py
Normal 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
214
agents/views.py
Normal 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)
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
397
static/js/agents-core.js
Normal 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();
|
||||||
|
});
|
||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user