From 5eba8fee849f5ebcba0cee2b7ce5f75a22724b6b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 31 Jul 2025 19:05:09 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Complete=20agents=20app=20implem?= =?UTF-8?q?entation=20with=20social=20ads=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- AGENTS_APP_RECREATION_GUIDE.md | 639 ++++++++++++++++++ agents/__init__.py | 0 agents/admin.py | 24 + agents/apps.py | 6 + agents/management/__init__.py | 0 agents/management/commands/__init__.py | 0 .../commands/create_sample_agents.py | 151 +++++ .../commands/create_social_ads_agent.py | 101 +++ agents/migrations/0001_initial.py | 140 ++++ agents/migrations/__init__.py | 0 agents/models.py | 64 ++ agents/serializers.py | 28 + agents/templates/agents/agent_detail.html | 368 ++++++++++ agents/templates/agents/marketplace.html | 348 ++++++++++ agents/tests.py | 3 + agents/urls.py | 19 + agents/views.py | 214 ++++++ netcop_hub/settings.py | 1 + netcop_hub/urls.py | 5 +- static/js/agents-core.js | 397 +++++++++++ workflows/config/agents.py | 1 + workflows/views.py | 13 +- 22 files changed, 2517 insertions(+), 5 deletions(-) create mode 100644 AGENTS_APP_RECREATION_GUIDE.md create mode 100644 agents/__init__.py create mode 100644 agents/admin.py create mode 100644 agents/apps.py create mode 100644 agents/management/__init__.py create mode 100644 agents/management/commands/__init__.py create mode 100644 agents/management/commands/create_sample_agents.py create mode 100644 agents/management/commands/create_social_ads_agent.py create mode 100644 agents/migrations/0001_initial.py create mode 100644 agents/migrations/__init__.py create mode 100644 agents/models.py create mode 100644 agents/serializers.py create mode 100644 agents/templates/agents/agent_detail.html create mode 100644 agents/templates/agents/marketplace.html create mode 100644 agents/tests.py create mode 100644 agents/urls.py create mode 100644 agents/views.py create mode 100644 static/js/agents-core.js diff --git a/AGENTS_APP_RECREATION_GUIDE.md b/AGENTS_APP_RECREATION_GUIDE.md new file mode 100644 index 0000000..0ce280d --- /dev/null +++ b/AGENTS_APP_RECREATION_GUIDE.md @@ -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('/', views.agent_detail, name='agent_detail'), + path('execute/', views.execute_agent, name='execute_agent'), + path('executions/', views.execution_list, name='execution_list'), + path('executions//', 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. \ No newline at end of file diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agents/admin.py b/agents/admin.py new file mode 100644 index 0000000..9bcf368 --- /dev/null +++ b/agents/admin.py @@ -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'] diff --git a/agents/apps.py b/agents/apps.py new file mode 100644 index 0000000..5e1f15f --- /dev/null +++ b/agents/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + +class AgentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'agents' + verbose_name = 'Agents' diff --git a/agents/management/__init__.py b/agents/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agents/management/commands/__init__.py b/agents/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agents/management/commands/create_sample_agents.py b/agents/management/commands/create_sample_agents.py new file mode 100644 index 0000000..25671d1 --- /dev/null +++ b/agents/management/commands/create_sample_agents.py @@ -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()}') \ No newline at end of file diff --git a/agents/management/commands/create_social_ads_agent.py b/agents/management/commands/create_social_ads_agent.py new file mode 100644 index 0000000..dfefd92 --- /dev/null +++ b/agents/management/commands/create_social_ads_agent.py @@ -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') \ No newline at end of file diff --git a/agents/migrations/0001_initial.py b/agents/migrations/0001_initial.py new file mode 100644 index 0000000..9275a54 --- /dev/null +++ b/agents/migrations/0001_initial.py @@ -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"], + }, + ), + ] diff --git a/agents/migrations/__init__.py b/agents/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agents/models.py b/agents/models.py new file mode 100644 index 0000000..e5537df --- /dev/null +++ b/agents/models.py @@ -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}" diff --git a/agents/serializers.py b/agents/serializers.py new file mode 100644 index 0000000..bd8ca65 --- /dev/null +++ b/agents/serializers.py @@ -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' + ] \ No newline at end of file diff --git a/agents/templates/agents/agent_detail.html b/agents/templates/agents/agent_detail.html new file mode 100644 index 0000000..09d8dd9 --- /dev/null +++ b/agents/templates/agents/agent_detail.html @@ -0,0 +1,368 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}{{ agent.name }} - Quantum Tasks AI{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} + + +
+ + {% include "workflows/components/agent_header.html" with agent_title=agent.name agent_subtitle=agent.short_description %} + + + {% include "workflows/components/quick_agents_panel.html" %} + + +
+ +
+
+

+ {{ agent.category.icon }} + {{ agent.name }} Details +

+
+
+
+ {% csrf_token %} + + +
+

{{ agent.category.icon }} {{ agent.name }} Configuration

+ + {% for field in agent.form_schema.fields %} +
+ + + {% if field.type == 'textarea' %} + + + {% elif field.type == 'select' %} + + + {% elif field.type == 'text' %} + + + {% elif field.type == 'url' %} + + + {% elif field.type == 'checkbox' %} + + {% endif %} + + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + +
+ {% endfor %} +
+ + +
+ {% if user.is_authenticated %} + {% if user.wallet_balance >= agent.price %} + + {% else %} +
+ Insufficient balance! You need {{ agent.price }} AED. +
+ + πŸ’° Top Up Wallet + + {% endif %} + {% else %} + + πŸ” Login to Continue + + {% endif %} +
+
+
+
+ + + {% include "workflows/components/how_it_works_widget.html" with steps="agents" %} +
+ + + {% include "workflows/components/processing_status.html" with status_title="Processing..." status_text="Please wait while we execute your agent..." %} + + + {% include "workflows/components/results_container.html" with results_title="Agent Results" %} +
+{% endblock %} + +{% block extra_js %} + + +{% endblock %} \ No newline at end of file diff --git a/agents/templates/agents/marketplace.html b/agents/templates/agents/marketplace.html new file mode 100644 index 0000000..e8b5003 --- /dev/null +++ b/agents/templates/agents/marketplace.html @@ -0,0 +1,348 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}AI Agents Marketplace - Quantum Tasks AI{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block content %} +
+ +
+

πŸ€– AI Agents Marketplace

+

Discover powerful AI agents to automate your tasks and boost productivity

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

{{ agents.count }} agent{{ agents.count|pluralize }} found

+
+ + +
+ {% for agent in agents %} +
+
+
{{ agent.category.icon }}
+
+

{{ agent.name }}

+ {{ agent.category.name }} +
+
{{ agent.price }} AED
+
+ +
+

{{ agent.short_description }}

+
+ + +
+ {% empty %} +
+
πŸ€–
+

No agents found

+

Try adjusting your search or filter criteria

+ View All Agents +
+ {% endfor %} +
+ + + {% if not selected_category and not search_query %} + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/agents/tests.py b/agents/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/agents/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/agents/urls.py b/agents/urls.py new file mode 100644 index 0000000..ad7de97 --- /dev/null +++ b/agents/urls.py @@ -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//', views.execution_detail, name='execution_detail'), + path('api/', views.agent_list, name='agent_list'), + path('api//', views.agent_detail, name='agent_detail_api'), + + # Agent detail page (must be last to avoid conflicts) + path('/', views.agent_detail_view, name='detail'), +] \ No newline at end of file diff --git a/agents/views.py b/agents/views.py new file mode 100644 index 0000000..de9ef52 --- /dev/null +++ b/agents/views.py @@ -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) diff --git a/netcop_hub/settings.py b/netcop_hub/settings.py index 81ec2d1..c1c9fd8 100644 --- a/netcop_hub/settings.py +++ b/netcop_hub/settings.py @@ -76,6 +76,7 @@ INSTALLED_APPS = [ 'wallet', 'core', 'workflows', # Unified workflows app (includes marketplace and agent execution) + 'agents', # New REST API-based agents system ] # Development apps (only in DEBUG mode) diff --git a/netcop_hub/urls.py b/netcop_hub/urls.py index 2e6ba57..e28336b 100644 --- a/netcop_hub/urls.py +++ b/netcop_hub/urls.py @@ -25,7 +25,10 @@ urlpatterns = [ path('wallet/', include('wallet.urls')), # 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')), ] diff --git a/static/js/agents-core.js b/static/js/agents-core.js new file mode 100644 index 0000000..4378293 --- /dev/null +++ b/static/js/agents-core.js @@ -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(); +}); \ No newline at end of file diff --git a/workflows/config/agents.py b/workflows/config/agents.py index 083f498..4b20cd5 100644 --- a/workflows/config/agents.py +++ b/workflows/config/agents.py @@ -27,6 +27,7 @@ AGENT_CONFIGS = { 'icon': 'πŸ“Š', 'webhook_url': 'http://localhost:5678/webhook/simple-pdf-processor', }, + } diff --git a/workflows/views.py b/workflows/views.py index edb4896..cd0d4c5 100644 --- a/workflows/views.py +++ b/workflows/views.py @@ -258,16 +258,21 @@ def process_workflow_request(request, agent_slug, agent_config, agent): status='processing' ) - # Actually send request to webhook (especially for data-analyzer) - logger.info(f"πŸ” DEBUG: Checking webhook conditions - agent_slug={agent_slug}, has_file={'file' in uploaded_files}") - if agent_slug == 'data-analyzer' and 'file' in uploaded_files: + # Actually send request to webhook (for agents that support file upload) + logger.info(f"πŸ” DEBUG: Checking webhook conditions - agent_slug={agent_slug}, has_file={'document_file' in uploaded_files or '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: # Check if this is an AJAX request (like the original system) if request.headers.get('X-Requested-With') == 'XMLHttpRequest': # 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( agent_config['webhook_url'], - uploaded_files['file'], + uploaded_file, form_data )