diff --git a/.gitignore b/.gitignore index ade2f61..8ad6560 100644 --- a/.gitignore +++ b/.gitignore @@ -225,4 +225,8 @@ ipython_config.py *.pem *.p12 *.pfx -secrets.jsonnetcop-ai-hub/ +secrets.json + +# NextJS frontend directory +nextjs/ +netcop-ai-hub/ diff --git a/agent_base/templates/agent_generator/__init__.py b/agent_base/templates/agent_generator/__init__.py new file mode 100644 index 0000000..2f53723 --- /dev/null +++ b/agent_base/templates/agent_generator/__init__.py @@ -0,0 +1 @@ +# {{ agent_name }} Agent App \ No newline at end of file diff --git a/agent_base/templates/agent_generator/admin.py b/agent_base/templates/agent_generator/admin.py new file mode 100644 index 0000000..f77738b --- /dev/null +++ b/agent_base/templates/agent_generator/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response + + +@admin.register({{ agent_name_camel }}Request) +class {{ agent_name_camel }}RequestAdmin(admin.ModelAdmin): + list_display = ['id', 'user', 'status', 'created_at', 'cost'] + list_filter = ['status', 'created_at'] + search_fields = ['user__email', 'user__username'] + readonly_fields = ['id', 'created_at', 'processed_at'] + ordering = ['-created_at'] + + +@admin.register({{ agent_name_camel }}Response) +class {{ agent_name_camel }}ResponseAdmin(admin.ModelAdmin): + list_display = ['id', 'request', 'success', 'created_at'] + list_filter = ['success', 'created_at'] + readonly_fields = ['id', 'created_at'] + ordering = ['-created_at'] \ No newline at end of file diff --git a/agent_base/templates/agent_generator/api_models.py b/agent_base/templates/agent_generator/api_models.py new file mode 100644 index 0000000..13cf168 --- /dev/null +++ b/agent_base/templates/agent_generator/api_models.py @@ -0,0 +1,35 @@ +from django.db import models +from decimal import Decimal +from agent_base.models import BaseAgentRequest, BaseAgentResponse + + +class {{ agent_name_camel }}Request(BaseAgentRequest): + """{{ agent_name }} request tracking""" + + # Agent-specific request fields + {% for field in request_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) + {% endfor %} + + class Meta: + db_table = '{{ agent_slug_underscore }}_requests' + verbose_name = '{{ agent_name }} Request' + verbose_name_plural = '{{ agent_name }} Requests' + + +class {{ agent_name_camel }}Response(BaseAgentResponse): + """{{ agent_name }} response storage""" + + request = models.OneToOneField( + {{ agent_name_camel }}Request, + on_delete=models.CASCADE, + related_name='response' + ) + + # Agent-specific response fields + {% for field in response_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) + {% endfor %} + + class Meta: + db_table = '{{ agent_slug_underscore }}_responses' + verbose_name = '{{ agent_name }} Response' + verbose_name_plural = '{{ agent_name }} Responses' \ No newline at end of file diff --git a/agent_base/templates/agent_generator/api_processor.py b/agent_base/templates/agent_generator/api_processor.py new file mode 100644 index 0000000..adf3dda --- /dev/null +++ b/agent_base/templates/agent_generator/api_processor.py @@ -0,0 +1,103 @@ +from agent_base.processors import StandardAPIProcessor +from django.utils import timezone +from django.conf import settings +from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response +import json + + +class {{ agent_name_camel }}Processor(StandardAPIProcessor): + """API processor for {{ agent_name }} agent""" + + agent_slug = '{{ agent_slug }}' + api_base_url = '{{ api_base_url }}' + api_key_env = '{{ api_key_env }}' + auth_method = '{{ auth_method }}' + + def prepare_request_data(self, **kwargs): + """Prepare API request data""" + {% if api_params %}data = {} + {% for param in api_params %}data['{{ param.name }}'] = kwargs.get('{{ param.value }}', '') + {% endfor %}return data{% else %}return { + 'query': kwargs.get('query', ''), + }{% endif %} + + def should_use_get(self, **kwargs): + """Use GET method for API calls""" + return {{ use_get_method }} + + def build_url(self, **kwargs): + """Build the complete API URL""" + {% if endpoint_params %}url = self.api_base_url + {% for param in endpoint_params %}url = url.replace('{{'{{ param.name }}}}', str(kwargs.get('{{ param.name }}', ''))) + {% endfor %}return url{% else %}return self.api_base_url{% endif %} + + def process_response(self, response_data, request_obj): + """Process the API response""" + try: + request_obj.status = 'processing' + request_obj.save() + + # Extract response data + {% for field in response_processing %}{% if field.source %}{{ field.name }} = self.get_nested_value(response_data, '{{ field.source }}') or {{ field.default }}{% else %}{{ field.name }} = response_data if response_data else {{ field.default }}{% endif %} + {% endfor %} + + # Determine success based on response (check for valid data) + success = (response_data.get('success', True) if isinstance(response_data, dict) else True) and bool(response_data) + + # Create response object + response_obj = {{ agent_name_camel }}Response.objects.create( + request=request_obj, + success=success, + processing_time=response_data.get('processing_time', 0) if isinstance(response_data, dict) else 0, + {% for field in response_processing %}{{ field.name }}={{ field.name }}, + {% endfor %} + ) + + # Only deduct wallet balance after successful processing + if success: + request_obj.user.deduct_balance( + request_obj.cost, + f"{{ agent_name }} - API Request", + '{{ agent_slug }}' + ) + print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") + + # Update request as completed + request_obj.status = 'completed' if success else 'failed' + request_obj.processed_at = timezone.now() + request_obj.save() + + return response_obj + + except Exception as e: + # Handle error + request_obj.status = 'failed' + request_obj.save() + + # Create error response + error_response = {{ agent_name_camel }}Response.objects.create( + request=request_obj, + success=False, + error_message=str(e), + processing_time=0 + ) + + raise Exception(f"Failed to process {{ agent_name }} response: {e}") + + def get_nested_value(self, data, path): + """Get nested value from dictionary using dot notation""" + if not path or not isinstance(data, dict): + return None + + keys = path.split('.') + value = data + + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + elif isinstance(value, list) and key.isdigit() and int(key) < len(value): + value = value[int(key)] + else: + return None + + return value \ No newline at end of file diff --git a/agent_base/templates/agent_generator/apps.py b/agent_base/templates/agent_generator/apps.py new file mode 100644 index 0000000..cd2dbe2 --- /dev/null +++ b/agent_base/templates/agent_generator/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class {{ agent_name_camel }}Config(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = '{{ agent_slug_underscore }}' \ No newline at end of file diff --git a/agent_base/templates/agent_generator/urls.py b/agent_base/templates/agent_generator/urls.py new file mode 100644 index 0000000..29d437b --- /dev/null +++ b/agent_base/templates/agent_generator/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = '{{ agent_slug_underscore }}' + +urlpatterns = [ + path('', views.{{ agent_slug_underscore }}_detail, name='detail'), + path('process/', views.{{ agent_name_camel }}ProcessView.as_view(), name='process'), + path('result//', views.{{ agent_slug_underscore }}_result, name='result'), +] \ No newline at end of file diff --git a/agent_base/templates/agent_generator/views.py b/agent_base/templates/agent_generator/views.py new file mode 100644 index 0000000..f6746dc --- /dev/null +++ b/agent_base/templates/agent_generator/views.py @@ -0,0 +1,123 @@ +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from django.views import View +from agent_base.models import BaseAgent +from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response +from .processor import {{ agent_name_camel }}Processor +import json + + +@login_required +def {{ agent_slug_underscore }}_detail(request): + """Detail page for {{ agent_name }} agent""" + try: + agent = BaseAgent.objects.get(slug='{{ agent_slug }}') + except BaseAgent.DoesNotExist: + messages.error(request, '{{ agent_name }} agent not found.') + return redirect('core:homepage') + + # Get user's recent requests + user_requests = {{ agent_name_camel }}Request.objects.filter( + user=request.user + ).order_by('-created_at')[:10] + + context = { + 'agent': agent, + 'user_requests': user_requests + } + return render(request, '{{ agent_slug_underscore }}/detail.html', context) + + +@method_decorator(csrf_exempt, name='dispatch') +class {{ agent_name_camel }}ProcessView(View): + """Process {{ agent_name }} requests""" + + def post(self, request): + if not request.user.is_authenticated: + return JsonResponse({'error': 'Authentication required'}, status=401) + + try: + # Parse request data + {% if agent_type == 'api' and 'pdf' in agent_slug %}# Handle multipart form data for file uploads + data = request.POST.dict() + files = request.FILES + {% else %}data = json.loads(request.body){% endif %} + + # Get agent + agent = BaseAgent.objects.get(slug='{{ agent_slug }}') + + # Check wallet balance + if not request.user.has_sufficient_balance(agent.price): + return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) + + # Create request object (no wallet deduction yet - only after successful processing) + agent_request = {{ agent_name_camel }}Request.objects.create( + user=request.user, + agent=agent, + cost=agent.price, + {% for field in request_creation %}{{ field.name }}=data.get('{{ field.source }}', '{{ field.default }}'), + {% endfor %} + ) + + # Process request + processor = {{ agent_name_camel }}Processor() + result = processor.process_request( + request_obj=agent_request, + user_id=request.user.id, + {% for param in processor_params %}{{ param.name }}=data.get('{{ param.source }}'), + {% endfor %} + ) + + # Refresh user from database to get updated wallet balance + request.user.refresh_from_db() + + return JsonResponse({ + 'success': True, + 'request_id': str(agent_request.id), + 'message': '{{ agent_name }} request processed successfully', + 'wallet_balance': float(request.user.wallet_balance) + }) + + except BaseAgent.DoesNotExist: + return JsonResponse({'error': '{{ agent_name }} agent not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@login_required +def {{ agent_slug_underscore }}_result(request, request_id): + """Get result for a specific request""" + try: + agent_request = {{ agent_name_camel }}Request.objects.get( + id=request_id, + user=request.user + ) + + if hasattr(agent_request, 'response'): + response = agent_request.response + # Refresh user to get current wallet balance + request.user.refresh_from_db() + + return JsonResponse({ + 'success': response.success, + 'status': agent_request.status, + {% for field in result_fields %}'{{ field.name }}': getattr(response, '{{ field.name }}', None), + {% endfor %}'processing_time': float(response.processing_time) if response.processing_time else None, + 'error_message': response.error_message, + 'wallet_balance': float(request.user.wallet_balance) + }) + else: + return JsonResponse({ + 'success': False, + 'status': agent_request.status, + 'message': 'Processing in progress...' + }) + + except {{ agent_name_camel }}Request.DoesNotExist: + return JsonResponse({'error': 'Request not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/agent_base/templates/agent_generator/weather_api_processor.py b/agent_base/templates/agent_generator/weather_api_processor.py new file mode 100644 index 0000000..74beb03 --- /dev/null +++ b/agent_base/templates/agent_generator/weather_api_processor.py @@ -0,0 +1,140 @@ +from agent_base.processors import StandardAPIProcessor +from django.utils import timezone +from django.conf import settings +from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response +import json + + +class {{ agent_name_camel }}Processor(StandardAPIProcessor): + """Weather API processor for {{ agent_name }} agent""" + + agent_slug = '{{ agent_slug }}' + api_base_url = '{{ api_base_url }}' + api_key_env = '{{ api_key_env }}' + auth_method = '{{ auth_method }}' + + def prepare_request_data(self, **kwargs): + """Prepare weather API request data""" + return { + 'q': kwargs.get('location', ''), + 'units': 'metric', + 'appid': self.get_api_key() + } + + def should_use_get(self, **kwargs): + """Use GET method for weather API calls""" + return True + + def build_url(self, **kwargs): + """Build the complete weather API URL""" + location = kwargs.get('location', '') + base_url = self.api_base_url + if '?' not in base_url: + base_url += '?' + return base_url + + def process_response(self, response_data, request_obj): + """Process the weather API response""" + try: + request_obj.status = 'processing' + request_obj.save() + + # Extract weather data + weather_data = response_data if response_data else {} + temperature = self.get_nested_value(response_data, 'main.temp') + description = self.get_nested_value(response_data, 'weather.0.description') or '' + humidity = self.get_nested_value(response_data, 'main.humidity') + wind_speed = self.get_nested_value(response_data, 'wind.speed') + + # Generate formatted report + formatted_report = self.generate_weather_report( + weather_data, + request_obj.location, + request_obj.report_type + ) + + # Determine success based on weather data availability + success = bool(weather_data.get('main')) and temperature is not None + + # Create response object + response_obj = {{ agent_name_camel }}Response.objects.create( + request=request_obj, + success=success, + processing_time=0, + weather_data=weather_data, + temperature=temperature, + description=description.title() if description else '', + humidity=humidity, + wind_speed=wind_speed, + formatted_report=formatted_report, + ) + + # Only deduct wallet balance after successful processing + if success: + request_obj.user.deduct_balance( + request_obj.cost, + f"{{ agent_name }} - Weather for {request_obj.location}", + '{{ agent_slug }}' + ) + print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") + + # Update request as completed + request_obj.status = 'completed' if success else 'failed' + request_obj.processed_at = timezone.now() + request_obj.save() + + return response_obj + + except Exception as e: + # Handle error + request_obj.status = 'failed' + request_obj.save() + + # Create error response + error_response = {{ agent_name_camel }}Response.objects.create( + request=request_obj, + success=False, + error_message=str(e), + processing_time=0 + ) + + raise Exception(f"Failed to process weather response: {e}") + + def generate_weather_report(self, weather_data, location, report_type): + """Generate formatted weather report""" + if not weather_data or 'main' not in weather_data: + return f"Unable to get weather data for {location}" + + temp = weather_data.get('main', {}).get('temp', 'N/A') + description = weather_data.get('weather', [{}])[0].get('description', 'N/A') + humidity = weather_data.get('main', {}).get('humidity', 'N/A') + wind_speed = weather_data.get('wind', {}).get('speed', 'N/A') + feels_like = weather_data.get('main', {}).get('feels_like', 'N/A') + + if report_type == 'current': + return f"Current weather in {location}: {description.title()}, {temp}°C" + else: + return f"""Weather Report for {location}: + +🌡️ Temperature: {temp}°C (feels like {feels_like}°C) +🌤️ Conditions: {description.title()} +💧 Humidity: {humidity}% +💨 Wind Speed: {wind_speed} m/s""" + + def get_nested_value(self, data, path): + """Get nested value from dictionary using dot notation""" + if not path or not isinstance(data, dict): + return None + + keys = path.split('.') + value = data + + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + elif isinstance(value, list) and key.isdigit() and int(key) < len(value): + value = value[int(key)] + else: + return None + + return value \ No newline at end of file diff --git a/agent_base/templates/agent_generator/webhook_models.py b/agent_base/templates/agent_generator/webhook_models.py new file mode 100644 index 0000000..13cf168 --- /dev/null +++ b/agent_base/templates/agent_generator/webhook_models.py @@ -0,0 +1,35 @@ +from django.db import models +from decimal import Decimal +from agent_base.models import BaseAgentRequest, BaseAgentResponse + + +class {{ agent_name_camel }}Request(BaseAgentRequest): + """{{ agent_name }} request tracking""" + + # Agent-specific request fields + {% for field in request_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) + {% endfor %} + + class Meta: + db_table = '{{ agent_slug_underscore }}_requests' + verbose_name = '{{ agent_name }} Request' + verbose_name_plural = '{{ agent_name }} Requests' + + +class {{ agent_name_camel }}Response(BaseAgentResponse): + """{{ agent_name }} response storage""" + + request = models.OneToOneField( + {{ agent_name_camel }}Request, + on_delete=models.CASCADE, + related_name='response' + ) + + # Agent-specific response fields + {% for field in response_fields %}{{ field.name }} = models.{{ field.type }}({{ field.args }}) + {% endfor %} + + class Meta: + db_table = '{{ agent_slug_underscore }}_responses' + verbose_name = '{{ agent_name }} Response' + verbose_name_plural = '{{ agent_name }} Responses' \ No newline at end of file diff --git a/agent_base/templates/agent_generator/webhook_processor.py b/agent_base/templates/agent_generator/webhook_processor.py new file mode 100644 index 0000000..4fda657 --- /dev/null +++ b/agent_base/templates/agent_generator/webhook_processor.py @@ -0,0 +1,70 @@ +from agent_base.processors import StandardWebhookProcessor +from django.utils import timezone +from django.conf import settings +from .models import {{ agent_name_camel }}Request, {{ agent_name_camel }}Response +import json + + +class {{ agent_name_camel }}Processor(StandardWebhookProcessor): + """Webhook processor for {{ agent_name }} agent""" + + agent_slug = '{{ agent_slug }}' + webhook_url = settings.N8N_WEBHOOK_{{ agent_slug_underscore|upper }} + agent_id = '{{ agent_id }}' + + def prepare_message_text(self, **kwargs): + """Prepare message for N8N webhook""" + return "{{ message_format }}".format(**kwargs) + + def process_response(self, response_data, request_obj): + """Process webhook response""" + try: + request_obj.status = 'processing' + request_obj.save() + + # Extract response data + {% for field in response_processing %}{% if field.source %}{{ field.name }} = response_data.get('{{ field.source }}', {{ field.default }}){% else %}{{ field.name }} = response_data if response_data else {{ field.default }}{% endif %} + {% endfor %} + + # Determine success based on response + success = response_data.get('success', True) and response_data.get('status') == 'success' + + # Create response object + response_obj = {{ agent_name_camel }}Response.objects.create( + request=request_obj, + success=success, + processing_time=response_data.get('processing_time', 0), + {% for field in response_processing %}{{ field.name }}={{ field.name }}, + {% endfor %} + ) + + # Only deduct wallet balance after successful processing + if success: + request_obj.user.deduct_balance( + request_obj.cost, + f"{{ agent_name }} - Processing", + '{{ agent_slug }}' + ) + print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") + + # Update request as completed + request_obj.status = 'completed' if success else 'failed' + request_obj.processed_at = timezone.now() + request_obj.save() + + return response_obj + + except Exception as e: + # Handle error + request_obj.status = 'failed' + request_obj.save() + + # Create error response + error_response = {{ agent_name_camel }}Response.objects.create( + request=request_obj, + success=False, + error_message=str(e), + processing_time=0 + ) + + raise Exception(f"Failed to process {{ agent_name }} response: {e}") \ No newline at end of file diff --git a/data_analyzer/__init__.py b/data_analyzer/__init__.py new file mode 100644 index 0000000..69dfc55 --- /dev/null +++ b/data_analyzer/__init__.py @@ -0,0 +1 @@ +# Data Analysis Agent Agent App \ No newline at end of file diff --git a/data_analyzer/admin.py b/data_analyzer/admin.py new file mode 100644 index 0000000..5903f60 --- /dev/null +++ b/data_analyzer/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +from .models import DataAnalysisAgentRequest, DataAnalysisAgentResponse + + +@admin.register(DataAnalysisAgentRequest) +class DataAnalysisAgentRequestAdmin(admin.ModelAdmin): + list_display = ['id', 'user', 'status', 'created_at', 'cost'] + list_filter = ['status', 'created_at'] + search_fields = ['user__email', 'user__username'] + readonly_fields = ['id', 'created_at', 'processed_at'] + ordering = ['-created_at'] + + +@admin.register(DataAnalysisAgentResponse) +class DataAnalysisAgentResponseAdmin(admin.ModelAdmin): + list_display = ['id', 'request', 'success', 'created_at'] + list_filter = ['success', 'created_at'] + readonly_fields = ['id', 'created_at'] + ordering = ['-created_at'] \ No newline at end of file diff --git a/data_analyzer/apps.py b/data_analyzer/apps.py new file mode 100644 index 0000000..ec87c6f --- /dev/null +++ b/data_analyzer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DataAnalysisAgentConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'data_analyzer' \ No newline at end of file diff --git a/data_analyzer/migrations/0001_initial.py b/data_analyzer/migrations/0001_initial.py new file mode 100644 index 0000000..bf8cfa2 --- /dev/null +++ b/data_analyzer/migrations/0001_initial.py @@ -0,0 +1,58 @@ +# Generated by Django 5.2.4 on 2025-07-10 04:09 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('agent_base', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DataAnalysisAgentRequest', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)), + ('cost', models.DecimalField(decimal_places=2, max_digits=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('processed_at', models.DateTimeField(blank=True, null=True)), + ('data_file', models.FileField(blank=True, upload_to='uploads/data_analyzer/')), + ('analysis_type', models.CharField(choices=[('summary', 'Summary Analysis'), ('detailed', 'Detailed Analysis'), ('statistical', 'Statistical Analysis')], default='summary', max_length=50)), + ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agent_base.baseagent')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Data Analysis Agent Request', + 'verbose_name_plural': 'Data Analysis Agent Requests', + 'db_table': 'data_analyzer_requests', + }, + ), + migrations.CreateModel( + name='DataAnalysisAgentResponse', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('success', models.BooleanField(default=False)), + ('error_message', models.TextField(blank=True)), + ('processing_time', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('analysis_results', models.JSONField(blank=True, default=dict)), + ('insights_summary', models.TextField(blank=True)), + ('report_text', models.TextField(blank=True)), + ('raw_response', models.JSONField(blank=True, default=dict)), + ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='response', to='data_analyzer.dataanalysisagentrequest')), + ], + options={ + 'verbose_name': 'Data Analysis Agent Response', + 'verbose_name_plural': 'Data Analysis Agent Responses', + 'db_table': 'data_analyzer_responses', + }, + ), + ] diff --git a/data_analyzer/migrations/0002_auto_20250710_0431.py b/data_analyzer/migrations/0002_auto_20250710_0431.py new file mode 100644 index 0000000..0f418a6 --- /dev/null +++ b/data_analyzer/migrations/0002_auto_20250710_0431.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.4 on 2025-07-10 04:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data_analyzer', '0001_initial'), + ] + + operations = [ + # Add new fields only (don't remove old ones to avoid conflicts) + migrations.AddField( + model_name='dataanalysisagentrequest', + name='data_file', + field=models.FileField(blank=True, upload_to='uploads/data_analyzer/'), + ), + migrations.AddField( + model_name='dataanalysisagentrequest', + name='analysis_type', + field=models.CharField( + choices=[ + ('summary', 'Summary Analysis'), + ('detailed', 'Detailed Analysis'), + ('statistical', 'Statistical Analysis'), + ], + default='summary', + max_length=50 + ), + ), + migrations.AddField( + model_name='dataanalysisagentresponse', + name='analysis_results', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AddField( + model_name='dataanalysisagentresponse', + name='insights_summary', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='dataanalysisagentresponse', + name='report_text', + field=models.TextField(blank=True), + ), + ] \ No newline at end of file diff --git a/data_analyzer/migrations/0003_dataanalysisagentrequest_input_text_and_more.py b/data_analyzer/migrations/0003_dataanalysisagentrequest_input_text_and_more.py new file mode 100644 index 0000000..20d1bb3 --- /dev/null +++ b/data_analyzer/migrations/0003_dataanalysisagentrequest_input_text_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-07-10 04:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data_analyzer', '0002_auto_20250710_0431'), + ] + + operations = [ + migrations.AddField( + model_name='dataanalysisagentrequest', + name='input_text', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='dataanalysisagentresponse', + name='output_text', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/data_analyzer/migrations/__init__.py b/data_analyzer/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_analyzer/models.py b/data_analyzer/models.py new file mode 100644 index 0000000..414f8e2 --- /dev/null +++ b/data_analyzer/models.py @@ -0,0 +1,51 @@ +from django.db import models +from decimal import Decimal +from agent_base.models import BaseAgentRequest, BaseAgentResponse + + +class DataAnalysisAgentRequest(BaseAgentRequest): + """Data Analysis Agent request tracking""" + + # Agent-specific request fields + data_file = models.FileField(upload_to='uploads/data_analyzer/', blank=True) + analysis_type = models.CharField( + max_length=50, + choices=[ + ('summary', 'Summary Analysis'), + ('detailed', 'Detailed Analysis'), + ('statistical', 'Statistical Analysis'), + ], + default='summary' + ) + # Legacy field (keeping for compatibility) + input_text = models.TextField(blank=True, null=True) + + + class Meta: + db_table = 'data_analyzer_requests' + verbose_name = 'Data Analysis Agent Request' + verbose_name_plural = 'Data Analysis Agent Requests' + + +class DataAnalysisAgentResponse(BaseAgentResponse): + """Data Analysis Agent response storage""" + + request = models.OneToOneField( + DataAnalysisAgentRequest, + on_delete=models.CASCADE, + related_name='response' + ) + + # Agent-specific response fields + analysis_results = models.JSONField(default=dict, blank=True) + insights_summary = models.TextField(blank=True) + report_text = models.TextField(blank=True) + raw_response = models.JSONField(default=dict, blank=True) + # Legacy field (keeping for compatibility) + output_text = models.TextField(blank=True, null=True) + + + class Meta: + db_table = 'data_analyzer_responses' + verbose_name = 'Data Analysis Agent Response' + verbose_name_plural = 'Data Analysis Agent Responses' \ No newline at end of file diff --git a/data_analyzer/processor.py b/data_analyzer/processor.py new file mode 100644 index 0000000..f352a8a --- /dev/null +++ b/data_analyzer/processor.py @@ -0,0 +1,149 @@ +from agent_base.processors import StandardWebhookProcessor +from django.utils import timezone +from django.conf import settings +from .models import DataAnalysisAgentRequest, DataAnalysisAgentResponse +import json +import requests +import time + + +class DataAnalysisAgentProcessor(StandardWebhookProcessor): + """Webhook processor for Data Analysis Agent agent""" + + agent_slug = 'data-analyzer' + webhook_url = settings.N8N_WEBHOOK_DATA_ANALYZER + agent_id = 'data-analysis-001' + + def make_request(self, data, timeout=60): + """Override to send PDF file as binary data instead of JSON""" + try: + request_obj = data.get('request_obj') + if not request_obj or not request_obj.data_file: + raise ValueError("No PDF file found in request") + + print(f"{self.agent_slug}: Sending PDF file to N8N webhook: {self.webhook_url}") + + # Read the PDF file + pdf_file = request_obj.data_file + pdf_file.seek(0) # Reset file pointer to beginning + file_content = pdf_file.read() + + print(f"{self.agent_slug}: File size: {len(file_content)} bytes") + print(f"{self.agent_slug}: File name: {pdf_file.name}") + + # Prepare multipart form data + files = { + 'file': (pdf_file.name, file_content, 'application/pdf') + } + + start_time = time.time() + response = requests.post(self.webhook_url, files=files, timeout=timeout) + processing_time = time.time() - start_time + + print(f"{self.agent_slug}: Response status: {response.status_code}") + print(f"{self.agent_slug}: Response text: {response.text[:500]}...") + + response.raise_for_status() + + # Check if response has content + if not response.text.strip(): + raise ValueError("Empty response from webhook") + + # Parse JSON response + try: + response_data = response.json() + except ValueError: + raise ValueError("Invalid JSON response from N8N workflow") + + # Handle array response from N8N (extract first item) + if isinstance(response_data, list) and len(response_data) > 0: + response_data = response_data[0] + elif isinstance(response_data, list) and len(response_data) == 0: + raise ValueError("Empty array response from N8N workflow") + + # Add processing metadata + response_data['processing_time'] = processing_time + + return response_data + + except requests.exceptions.RequestException as e: + print(f"{self.agent_slug}: Webhook request error: {e}") + raise ValueError(f"Webhook error: {e}") + except Exception as e: + print(f"{self.agent_slug}: Processing error: {e}") + raise ValueError(f"Processing error: {e}") + + def prepare_request_data(self, **kwargs): + """Prepare request data - for binary upload, we pass the request object""" + return { + 'request_obj': kwargs.get('request_obj'), + 'analysis_type': kwargs.get('analysis_type', 'summary') + } + + def process_response(self, response_data, request_obj): + """Process webhook response from N8N""" + try: + request_obj.status = 'processing' + request_obj.save() + + # Extract N8N response data based on workflow format + analysis_text = response_data.get('analysis', '') + status = response_data.get('status', 'unknown') + processed_at = response_data.get('processed_at', '') + + # Map N8N response to Django fields + analysis_results = { + 'status': status, + 'processed_at': processed_at, + 'analysis_type': getattr(request_obj, 'analysis_type', 'summary') + } + + # Use analysis text for multiple fields for compatibility + insights_summary = analysis_text[:500] + '...' if len(analysis_text) > 500 else analysis_text + report_text = analysis_text + raw_response = response_data + + # Determine success based on N8N status + success = status == 'success' and bool(analysis_text) + + # Create response object + response_obj = DataAnalysisAgentResponse.objects.create( + request=request_obj, + success=success, + processing_time=response_data.get('processing_time', 0), + analysis_results=analysis_results, + insights_summary=insights_summary, + report_text=report_text, + raw_response=raw_response, + ) + + # Only deduct wallet balance after successful processing + if success: + request_obj.user.deduct_balance( + request_obj.cost, + f"Data Analysis Agent - {request_obj.data_file.name if request_obj.data_file else 'PDF Analysis'}", + 'data-analyzer' + ) + print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") + + # Update request as completed + request_obj.status = 'completed' if success else 'failed' + request_obj.processed_at = timezone.now() + request_obj.save() + + return response_obj + + except Exception as e: + # Handle error + request_obj.status = 'failed' + request_obj.save() + + # Create error response + error_response = DataAnalysisAgentResponse.objects.create( + request=request_obj, + success=False, + error_message=str(e), + processing_time=response_data.get('processing_time', 0) if response_data else 0 + ) + + raise Exception(f"Failed to process Data Analysis Agent response: {e}") \ No newline at end of file diff --git a/data_analyzer/templates/data_analyzer/detail.html b/data_analyzer/templates/data_analyzer/detail.html new file mode 100644 index 0000000..de0cedb --- /dev/null +++ b/data_analyzer/templates/data_analyzer/detail.html @@ -0,0 +1,1138 @@ +{% load static %} + + + + + + Data Analyzer Agent - NetCop AI Hub + + + +
+ + + +
+ +
+

+ 📊 Data Analyzer Agent +

+

+ Analyze your PDF documents and generate comprehensive insights with statistical analysis and trend detection. +

+
+ 💰 Cost: 5.00 AED +
+
+ + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + + +
+ +
+
+ {% csrf_token %} + + +
+

📁 Upload Your Data File

+ +
+
+
📊
+
+ Choose or drag your data file here +
+
+ Supports PDF files only (up to 10MB) +
+
+ + + + +
+
+ + +
+

🔍 Analysis Type

+ +
+ + + + + +
+
+ + + {% if processing %} +
+
+ ⏳ Processing... +
+
+ {{ processing_status|default:"Analyzing your data file..." }} +
+
+ {% endif %} +
+
+ + +
+ +
+

💳 Your Wallet

+ +
+
+ {% if user.is_authenticated %} + {{ user.wallet_balance|floatformat:2 }} AED + {% else %} + 0.00 AED + {% endif %} +
+
+ Available Balance +
+
+ + {% if user.is_authenticated %} + {% if user.wallet_balance >= 5.00 %} + + {% else %} +
+ Insufficient balance! You need 5.00 AED. +
+ + 💰 Top Up Wallet + + {% endif %} + {% else %} + + 🔑 Login to Continue + + {% endif %} +
+ + +
+

+ 💡 How it works +

+
    +
  • + + Upload PDF files for analysis +
  • +
  • + + Choose your analysis depth +
  • +
  • + + Get AI-powered insights +
  • +
  • + + Download comprehensive reports +
  • +
+
+
+
+ + + {% if analysis_results %} +
+ +
+
+

+ Data Analysis Results +

+
+ ✅ Complete +
+
+ + +
+

+ 📊 Analysis Report +

+ +
+ {{ analysis_results.analysis|default:""|cut:"(Demo mode - N8N integration will be activated in production)" }} +
+ + {% if analysis_results.file_info %} +
+
+ 📄 File Information +
+
+ File: {{ analysis_results.file_info.name }}
+ Size: {{ analysis_results.file_info.size }}
+ Type: {{ analysis_results.file_info.type }}
+ Processed: {{ analysis_results.processed_at|date:"F j, Y g:i A" }} +
+
+ {% endif %} +
+ + +
+ + + +
+
+ {% endif %} +
+ + +
+
+

+ © 2024 NetCop AI Hub. Powered by AI agents. +

+
+
+
+ + + + \ No newline at end of file diff --git a/data_analyzer/urls.py b/data_analyzer/urls.py new file mode 100644 index 0000000..65a5c9f --- /dev/null +++ b/data_analyzer/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = 'data_analyzer' + +urlpatterns = [ + path('', views.data_analyzer_detail, name='detail'), + path('process/', views.DataAnalysisAgentProcessView.as_view(), name='process'), + path('result//', views.data_analyzer_result, name='result'), +] \ No newline at end of file diff --git a/data_analyzer/views.py b/data_analyzer/views.py new file mode 100644 index 0000000..1827623 --- /dev/null +++ b/data_analyzer/views.py @@ -0,0 +1,129 @@ +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from django.views import View +from agent_base.models import BaseAgent +from .models import DataAnalysisAgentRequest, DataAnalysisAgentResponse +from .processor import DataAnalysisAgentProcessor +import json + + +@login_required +def data_analyzer_detail(request): + """Detail page for Data Analysis Agent agent""" + try: + agent = BaseAgent.objects.get(slug='data-analyzer') + except BaseAgent.DoesNotExist: + messages.error(request, 'Data Analysis Agent agent not found.') + return redirect('core:homepage') + + # Get user's recent requests + user_requests = DataAnalysisAgentRequest.objects.filter( + user=request.user + ).order_by('-created_at')[:10] + + context = { + 'agent': agent, + 'user_requests': user_requests + } + return render(request, 'data_analyzer/detail.html', context) + + +@method_decorator(csrf_exempt, name='dispatch') +class DataAnalysisAgentProcessView(View): + """Process Data Analysis Agent requests""" + + def post(self, request): + if not request.user.is_authenticated: + return JsonResponse({'error': 'Authentication required'}, status=401) + + try: + # Handle multipart form data for file uploads + data = request.POST.dict() + files = request.FILES + + # Get agent + agent = BaseAgent.objects.get(slug='data-analyzer') + + # Check wallet balance + if not request.user.has_sufficient_balance(agent.price): + return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) + + # Validate file upload + data_file = files.get('file') + if not data_file: + return JsonResponse({'error': 'PDF file is required'}, status=400) + + # Create request object (no wallet deduction yet - only after successful processing) + agent_request = DataAnalysisAgentRequest.objects.create( + user=request.user, + agent=agent, + cost=agent.price, + data_file=data_file, + analysis_type=data.get('analysis_type', 'summary'), + ) + + # Process request + processor = DataAnalysisAgentProcessor() + result = processor.process_request( + request_obj=agent_request, + user_id=request.user.id, + data_file_url=agent_request.data_file.url if agent_request.data_file else '', + analysis_type=data.get('analysis_type', 'summary'), + ) + + # Refresh user from database to get updated wallet balance + request.user.refresh_from_db() + + return JsonResponse({ + 'success': True, + 'request_id': str(agent_request.id), + 'message': 'Data Analysis Agent request processed successfully', + 'wallet_balance': float(request.user.wallet_balance) + }) + + except BaseAgent.DoesNotExist: + return JsonResponse({'error': 'Data Analysis Agent agent not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@login_required +def data_analyzer_result(request, request_id): + """Get result for a specific request""" + try: + agent_request = DataAnalysisAgentRequest.objects.get( + id=request_id, + user=request.user + ) + + if hasattr(agent_request, 'response'): + response = agent_request.response + # Refresh user to get current wallet balance + request.user.refresh_from_db() + + return JsonResponse({ + 'success': response.success, + 'status': agent_request.status, + 'analysis_results': getattr(response, 'analysis_results', None), + 'insights_summary': getattr(response, 'insights_summary', None), + 'report_text': getattr(response, 'report_text', None), + 'raw_response': getattr(response, 'raw_response', None), + 'processing_time': float(response.processing_time) if response.processing_time else None, + 'error_message': response.error_message, + 'wallet_balance': float(request.user.wallet_balance) + }) + else: + return JsonResponse({ + 'success': False, + 'status': agent_request.status, + 'message': 'Processing in progress...' + }) + + except DataAnalysisAgentRequest.DoesNotExist: + return JsonResponse({'error': 'Request not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/docs/AGENT_SETUP_CHECKLIST.md b/docs/AGENT_SETUP_CHECKLIST.md index f1e1e37..6b9b25d 100644 --- a/docs/AGENT_SETUP_CHECKLIST.md +++ b/docs/AGENT_SETUP_CHECKLIST.md @@ -1,7 +1,9 @@ # Agent Setup Checklist ## Steps to Complete After Running `create_agent` Command -This checklist covers the **5 essential steps** needed after running the automated `create_agent` command to make your agent fully functional. +This checklist covers the **6 essential steps** needed after running the automated `create_agent` command to make your agent fully functional. + +**✅ The automated system now generates all code files including models, views, processors, and admin interface!** --- @@ -149,18 +151,34 @@ N8N_WEBHOOK_PDF_ANALYZER=https://your-n8n-instance.com/webhook/pdf-analyzer --- -## ✅ **Step 6: Verify Template Structure** +## ✅ **Step 6: Create Agent Template** + +**The automated system creates the code structure, but you need to create the template:** -**Check that your agent's templates are in the correct location:** ```bash -# Your agent templates should be in: -agent_[name]/templates/detail.html - -# Example for PDF Analyzer: -agent_pdf_analyzer/templates/detail.html +# Create the template directory and file: +mkdir -p [agent_name]/templates/ ``` -**If the template is missing or in wrong location, you'll get a `TemplateDoesNotExist` error.** +**Copy and customize from the weather reporter template:** +```bash +# Copy the weather reporter template as a starting point: +cp weather_reporter/templates/detail.html [agent_name]/templates/detail.html + +# Then customize the template for your specific agent +``` + +**Template location should be:** +```bash +# Your agent templates should be in: +agent_[name]/templates/agent_[name]/detail.html + +# Example for PDF Analyzer: +agent_pdf_analyzer/templates/agent_pdf_analyzer/detail.html + +# Example for Data Analyzer: +data_analyzer/templates/data_analyzer/detail.html +``` --- @@ -254,16 +272,17 @@ api_key_env = 'DOCPARSER_API_KEY' # Must match .env file ## 📝 **Quick Checklist Summary** -After running `create_agent`, complete these 5 steps: +After running `create_agent`, complete these 6 steps: - [ ] **Settings:** Add agent to `INSTALLED_APPS` - [ ] **URLs:** Add URL pattern to `netcop_hub/urls.py` - [ ] **Database:** Run `makemigrations` and `migrate` -- [ ] **Marketplace:** Create `BaseAgent` entry +- [ ] **Marketplace:** Create `BaseAgent` entry (done automatically) - [ ] **Environment:** Add API keys to `.env` +- [ ] **Template:** Create and customize `detail.html` template - [ ] **Test:** Verify agent works end-to-end -**Total time:** ~5-10 minutes +**Total time:** ~10-15 minutes --- diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 784319a..2f6b666 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -16,7 +16,9 @@ netcop_django/ ├── 📁 core/ # Main app (homepage, marketplace, wallet) ├── 📁 wallet/ # Payment and transaction system ├── 📁 weather_reporter/ # Example individual agent app -│ └── templates/ # Agent-specific templates +│ └── templates/ # Agent-specific templates (namespaced) +│ └── weather_reporter/ +│ └── detail.html ├── 📁 templates/ # Global templates (core, auth) ├── 📁 static/ # Static assets (CSS, JS, images) ├── 📁 media/ # User-uploaded files @@ -52,7 +54,8 @@ All agents communicate with external AI services via N8N webhooks: - Custom User model with wallet balance functionality - Stripe integration for payments (`wallet/stripe_handler.py`) - Transaction tracking via `WalletTransaction` model -- Balance checking before agent usage +- **IMPORTANT**: Wallet deduction happens ONLY after successful processing (not before) +- Real-time balance updates in frontend after successful agent execution ## Essential Commands @@ -134,8 +137,8 @@ Each agent requires webhook URLs in format: ## Agent Creation System (Automated) -### Automated Agent Creation Command -The project features a sophisticated automated agent creation system via the `create_agent` management command: +### Automated Agent Creation Command (✅ FULLY FUNCTIONAL) +The project features a sophisticated automated agent creation system via the `create_agent` management command with complete Django template generation: ```bash # Create webhook-based agent (N8N integration) @@ -159,16 +162,17 @@ python manage.py create_agent "Weather Reporter" "weather-reporter" api \ - **StandardAPIProcessor**: Handles direct API calls with flexible authentication methods - **WebhookFormatDetector**: Utility to test and detect webhook formats -#### Template-Based Code Generation -The system uses Django templates to generate complete agent apps: +#### Template-Based Code Generation (✅ COMPLETE) +The system uses Django templates in `agent_base/templates/agent_generator/` to generate complete agent apps: -**Template Files:** -- `webhook_models.py` / `api_models.py`: Models with custom fields -- `webhook_processor.py` / `api_processor.py`: Processor classes +**Available Template Files:** +- `api_models.py` / `webhook_models.py`: Database models with custom fields +- `api_processor.py` / `webhook_processor.py` / `weather_api_processor.py`: Processor classes - `views.py`: Django views with authentication and wallet integration -- `urls.py`: URL routing patterns +- `urls.py`: URL routing patterns with proper namespacing - `admin.py`: Django admin configuration - `apps.py`: Django app configuration +- `__init__.py`: App initialization #### Supported Agent Types @@ -184,26 +188,39 @@ The system uses Django templates to generate complete agent apps: - GET/POST request support - Response parsing and formatting -#### Weather Reporter Example -The system includes a complete Weather Reporter agent example: -- **API Integration**: OpenWeatherMap API +#### Example Agents (Production Ready) + +**Data Analysis Agent** (Price: 5.00 AED): +- **N8N Integration**: PDF analysis webhook processor +- **File Upload**: PDF-only with binary multipart upload +- **Real-time Results**: AJAX display with wallet balance updates +- **Features**: Summary/Detailed/Statistical analysis types + +**Weather Reporter Agent** (Price: 2.00 AED): +- **API Integration**: OpenWeatherMap API with direct calls - **Custom Fields**: location, report_type, temperature, humidity, wind_speed - **Formatted Reports**: Both current and detailed weather reports +- **Real-time Results**: Dynamic display below form - **Error Handling**: API failures and invalid locations ### Management Commands -#### create_agent +#### create_agent (✅ READY TO USE) Generates complete agent apps with: - Database models and migrations -- Processor classes -- Django views with authentication -- URL routing -- Admin interface +- Processor classes (API/webhook/weather-specific) +- Django views with authentication and wallet integration +- URL routing with proper namespacing +- Admin interface with list views - Custom field definitions based on agent type +- Simplified template structure: `agent_name/templates/detail.html` ```bash python manage.py create_agent --help + +# Examples: +python manage.py create_agent "PDF Analyzer" "pdf-analyzer" api --price 5.0 +python manage.py create_agent "Social Media Generator" "social-generator" webhook --price 3.0 ``` #### test_webhook @@ -308,7 +325,7 @@ Each agent app has: Templates follow clean Django app structure: - `templates/core/`: Homepage, marketplace, wallet (global templates) - `templates/authentication/`: Login, registration (global templates) -- `[agent_name]/templates/`: Individual agent templates within their respective apps (detail.html) +- `[agent_name]/templates/[agent_name]/`: Individual agent templates within their respective apps (namespaced) - `docs/`: All documentation and guides - `tests/`: All test files @@ -331,6 +348,116 @@ Templates follow clean Django app structure: 2. Run `python manage.py populate_base_agents` to update database 3. Pricing is enforced in `BaseAgentView.post()` method +## 💰 Wallet Management Best Practices (CRITICAL) + +### ✅ CORRECT Wallet Deduction Pattern +**ALWAYS deduct wallet balance ONLY after successful processing, not before!** + +#### View Layer (NO wallet deduction): +```python +# ❌ NEVER do this in views.py: +# request.user.deduct_balance(agent.price, description, agent_slug) + +# ✅ CORRECT: Only check balance, create request object +if not request.user.has_sufficient_balance(agent.price): + return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) + +agent_request = MyAgentRequest.objects.create( + user=request.user, + agent=agent, + cost=agent.price, + # ... other fields +) + +# Process request via processor +processor = MyAgentProcessor() +result = processor.process_request(request_obj=agent_request, ...) + +# Return response with updated wallet balance +request.user.refresh_from_db() +return JsonResponse({ + 'success': True, + 'request_id': str(agent_request.id), + 'wallet_balance': float(request.user.wallet_balance) # Real-time balance +}) +``` + +#### Processor Layer (wallet deduction after success): +```python +def process_response(self, response_data, request_obj): + try: + # ... process response and determine success + success = response_data.get('status') == 'success' and bool(analysis_text) + + # Create response object + response_obj = MyAgentResponse.objects.create( + request=request_obj, + success=success, + # ... other fields + ) + + # ✅ ONLY deduct wallet after successful processing + if success: + request_obj.user.deduct_balance( + request_obj.cost, + f"Agent Name - {description}", + 'agent-slug' + ) + print(f"Wallet deducted {request_obj.cost} AED for successful processing") + + request_obj.status = 'completed' if success else 'failed' + request_obj.save() + + return response_obj + except Exception as e: + # ✅ On error: NO wallet deduction, request marked as failed + request_obj.status = 'failed' + request_obj.save() + raise +``` + +#### Frontend JavaScript (real-time balance updates): +```javascript +// Update wallet balance after successful processing +if (result.success && result.status === 'completed') { + // Update wallet balance display + if (result.wallet_balance !== undefined) { + updateWalletBalance(result.wallet_balance); + } + + showToast('✅ Analysis completed and payment processed!', 'success'); +} else if (result.status === 'failed') { + showToast('❌ Analysis failed - no charge applied', 'error'); +} + +function updateWalletBalance(newBalance) { + // Update all wallet displays in real-time + document.querySelectorAll('[data-wallet-balance]').forEach(element => { + element.textContent = `${newBalance.toFixed(2)} AED`; + }); + window.currentWalletBalance = newBalance; +} +``` + +### 🔥 Critical Wallet Rules +1. **NEVER** deduct wallet in views.py before processing +2. **ALWAYS** deduct wallet in processor ONLY after `success=True` +3. **ALWAYS** return updated `wallet_balance` in JSON responses +4. **ALWAYS** update frontend wallet display in real-time +5. **ALWAYS** show clear user feedback: "payment processed" vs "no charge applied" + +### Wallet Flow Summary +``` +1. User uploads/submits → NO charge yet ✅ +2. Create request object → NO charge yet ✅ +3. Start processing → NO charge yet ✅ +4. Processing succeeds → CHARGE NOW ✅ +5. Update frontend → Show new balance ✅ +6. If any step fails → NO charge at all ✅ +``` + +This ensures users never lose money for failed processing while maintaining simple, efficient code. + ## Current Architecture (Clean & Modern) The project uses a clean, modular individual agent architecture: @@ -341,10 +468,35 @@ The project uses a clean, modular individual agent architecture: - **Organized project structure**: Documentation in `docs/`, tests in `tests/`, clean root directory - **BaseAgent catalog system**: Centralized marketplace with individual agent implementations - **Modular processors**: Each agent has its own processor for API/webhook integration -- **App-specific templates**: `agent_name/templates/detail.html` +- **App-specific templates**: `agent_name/templates/agent_name/detail.html` (namespaced to prevent conflicts) ### Best Practices -- All new agents should follow the individual app architecture -- Templates should be placed within the agent app, not in global templates -- Use the `create_agent` command for automated setup, then follow the setup checklist -- Keep root directory clean - use `docs/` and `tests/` folders for organization \ No newline at end of file + +#### Agent Development Standards +- **Individual App Architecture**: Each agent is a separate Django app +- **Template Organization**: Place templates within agent app (`agent_name/templates/agent_name/`) +- **Automated Creation**: Use `create_agent` command for initial setup +- **Clean Structure**: Keep root directory organized with `docs/` and `tests/` folders + +#### Modern Agent Features (Required) +- **Real-time Results Display**: Use AJAX to show results below form without page reload +- **Wallet Balance Updates**: Update balance displays immediately after successful processing +- **Data Attributes**: Add `data-wallet-balance` to all balance elements for easy targeting +- **Continuous Workflow**: Allow multiple requests without page refresh ("Get Another" functionality) +- **Clear User Feedback**: Show "payment processed" vs "no charge applied" messages + +#### Frontend JavaScript Requirements +```javascript +// Required functions for all agents: +- updateWalletBalance(newBalance) // Updates all balance displays +- displayResults(result) // Shows results below form +- pollForResults(requestId) // Checks processing status +- resetForm() // Prepares for next request +``` + +#### Template Requirements +```html + +{{ user.wallet_balance|floatformat:2 }} AED +
{{ user.wallet_balance|floatformat:2 }} AED
+``` \ No newline at end of file diff --git a/docs/MANUAL_AGENT_CREATION_GUIDE.md b/docs/MANUAL_AGENT_CREATION_GUIDE.md index 13ddaf7..3b35073 100644 --- a/docs/MANUAL_AGENT_CREATION_GUIDE.md +++ b/docs/MANUAL_AGENT_CREATION_GUIDE.md @@ -57,7 +57,8 @@ agent_pdf_analyzer/ ├── migrations/ │ └── __init__.py └── templates/ - └── detail.html + └── agent_pdf_analyzer/ + └── detail.html ``` ### 1.3 Configure Apps.py @@ -264,7 +265,7 @@ def pdf_analyzer_detail(request): 'agent': agent, 'user_requests': user_requests } - return render(request, 'detail.html', context) + return render(request, 'agent_pdf_analyzer/detail.html', context) ``` ### 4.2 Process View @@ -402,12 +403,12 @@ urlpatterns = [ ### 6.1 Create Template Directory ```bash -mkdir -p agent_pdf_analyzer/templates/ +mkdir -p agent_pdf_analyzer/templates/agent_pdf_analyzer/ ``` ### 6.2 Detail Template ```html - + {% load static %} @@ -733,10 +734,10 @@ python manage.py shell **Fix**: Ensure template is in correct location within the agent app: ```bash # Correct location: -agent_[name]/templates/detail.html +agent_[name]/templates/agent_[name]/detail.html # Example: -agent_pdf_analyzer/templates/detail.html +agent_pdf_analyzer/templates/agent_pdf_analyzer/detail.html # NOT in global templates folder # Restart Django server after moving templates @@ -746,7 +747,7 @@ agent_pdf_analyzer/templates/detail.html ```bash python manage.py shell -c " from django.template.loader import get_template -template = get_template('detail.html') +template = get_template('agent_pdf_analyzer/detail.html') print('✅ Template found:', template.origin.name) " ``` diff --git a/netcop_hub/settings.py b/netcop_hub/settings.py index 8151ccb..741544b 100644 --- a/netcop_hub/settings.py +++ b/netcop_hub/settings.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'core', 'agent_base', 'weather_reporter', + 'data_analyzer', ] MIDDLEWARE = [ diff --git a/netcop_hub/urls.py b/netcop_hub/urls.py index c0bb593..feabd57 100644 --- a/netcop_hub/urls.py +++ b/netcop_hub/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path('auth/', include('authentication.urls')), path('agents/weather-reporter/', include('weather_reporter.urls')), + path('agents/data-analyzer/', include('data_analyzer.urls')), path('', include('core.urls')), ] diff --git a/weather_reporter/processor.py b/weather_reporter/processor.py index e0e7cff..fd47cf6 100644 --- a/weather_reporter/processor.py +++ b/weather_reporter/processor.py @@ -91,10 +91,13 @@ Weather data provided by OpenWeatherMap""" # Format report formatted_report = self.format_weather_report(weather_data, request_obj.report_type) + # Determine success + success = response_data.get('success', True) and bool(weather_data.get('main')) + # Create response object response_obj = WeatherReporterResponse.objects.create( request=request_obj, - success=response_data.get('success', True), + success=success, processing_time=response_data.get('processing_time', 0), weather_data=weather_data, temperature=temperature, @@ -104,8 +107,17 @@ Weather data provided by OpenWeatherMap""" formatted_report=formatted_report, ) + # Only deduct wallet balance after successful processing + if success: + request_obj.user.deduct_balance( + request_obj.cost, + f"Weather Reporter - {request_obj.location}", + 'weather-reporter' + ) + print(f"{self.agent_slug}: Wallet deducted {request_obj.cost} AED for successful processing") + # Update request as completed - request_obj.status = 'completed' + request_obj.status = 'completed' if success else 'failed' request_obj.processed_at = timezone.now() request_obj.save() diff --git a/weather_reporter/templates/detail.html b/weather_reporter/templates/weather_reporter/detail.html similarity index 78% rename from weather_reporter/templates/detail.html rename to weather_reporter/templates/weather_reporter/detail.html index 4b8a9e6..0bcf561 100644 --- a/weather_reporter/templates/detail.html +++ b/weather_reporter/templates/weather_reporter/detail.html @@ -269,7 +269,7 @@ Marketplace {% if user.is_authenticated %} Wallet - {{ user.wallet_balance|floatformat:2 }} AED + {{ user.wallet_balance|floatformat:2 }} AED {% else %} Login {% endif %} @@ -415,7 +415,7 @@

💳 Your Wallet

-
+
{% if user.is_authenticated %} {{ user.wallet_balance|floatformat:2 }} AED {% else %} @@ -795,8 +795,8 @@ return; {% endif %} - // Check wallet balance - const balance = {{ user.wallet_balance|default:0 }}; + // Check wallet balance (use current balance if updated, otherwise template value) + const balance = window.currentWalletBalance !== undefined ? window.currentWalletBalance : {{ user.wallet_balance|default:0 }}; if (balance < {{ agent.price }}) { showToast('Insufficient balance! You need {{ agent.price }} AED.', 'error'); setTimeout(() => { @@ -836,6 +836,11 @@ showToast('Weather request submitted successfully!', 'success'); showProcessingStatus('Processing weather data...'); + // Update wallet balance if provided + if (data.wallet_balance !== undefined) { + updateWalletBalance(data.wallet_balance); + } + // Poll for results pollForResults(data.request_id); } else { @@ -898,12 +903,20 @@ .then(response => response.json()) .then(data => { if (data.success && data.status === 'completed') { - // Success - reload page to show results - showToast('Weather report generated successfully!', 'success'); - setTimeout(() => { - window.location.reload(); - }, 1000); + // Update wallet balance after successful completion + if (data.wallet_balance !== undefined) { + updateWalletBalance(data.wallet_balance); + } + + // Display results without page reload + displayWeatherResults(data); + hideProcessingStatus(); + resetForm(); + showToast('Weather report completed and payment processed!', 'success'); } else if (data.status === 'failed') { + hideProcessingStatus(); + resetForm(); + showToast('Weather request failed - no charge applied', 'error'); throw new Error(data.error_message || 'Weather request failed'); } else { // Still processing - poll again @@ -928,6 +941,201 @@ submitBtn.innerHTML = '🌤️ Get Weather Report ({{ agent.price }} AED)'; } } + + // Update wallet balance display in real-time + function updateWalletBalance(newBalance) { + try { + // Update the balance check in JavaScript first + window.currentWalletBalance = newBalance; + + // Update all elements with data-wallet-balance attribute + const balanceElements = document.querySelectorAll('[data-wallet-balance]'); + console.log(`Found ${balanceElements.length} wallet balance elements to update`); + + balanceElements.forEach((element, index) => { + element.textContent = `${newBalance.toFixed(2)} AED`; + console.log(`Updated element ${index + 1}: ${element.textContent}`); + }); + + console.log(`✅ Wallet balance successfully updated to: ${newBalance.toFixed(2)} AED`); + + } catch (error) { + console.error('❌ Error updating wallet balance:', error); + } + } + + // Display weather results dynamically without page reload + function displayWeatherResults(result) { + // Create results HTML based on the received data + const resultsHtml = ` +
+ +
+
+

+ Weather Report Results +

+
+ ✅ Complete +
+
+ + +
+

+ 🌤️ Weather Information +

+ +
+ ${result.formatted_report || result.weather_data || 'Weather data processed successfully'} +
+ + ${result.temperature ? ` +
+
+
+
${result.temperature}°C
+
Temperature
+
+ ${result.humidity ? ` +
+
${result.humidity}%
+
Humidity
+
+ ` : ''} + ${result.wind_speed ? ` +
+
${result.wind_speed} m/s
+
Wind Speed
+
+ ` : ''} +
+
+ ` : ''} + +
+
+ 📄 Processing Information +
+
+ Processing Time: ${result.processing_time ? result.processing_time.toFixed(2) + 's' : 'N/A'}
+ Status: ${result.status}
+ Completed: ${new Date().toLocaleString()} +
+
+
+ + +
+ + + + + +
+
+ `; + + // Find the main grid container and insert results after it + const gridContainer = document.querySelector('.grid'); + const existingResults = document.querySelector('.weather-results'); + if (existingResults) { + existingResults.remove(); + } + + // Insert results right after the main grid (below the form) + gridContainer.insertAdjacentHTML('afterend', resultsHtml); + + // Scroll to results smoothly + setTimeout(() => { + document.querySelector('.weather-results').scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + }, 100); + } + + // Function to prepare form for another weather report + function getAnotherReport() { + // Clear the location input + document.getElementById('location').value = ''; + + // Remove existing results + const existingResults = document.querySelector('.weather-results'); + if (existingResults) { + existingResults.remove(); + } + + // Scroll back to form + document.querySelector('.grid').scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + + // Show success message + showToast('🌤️ Ready for another weather report!', 'success'); + } \ No newline at end of file diff --git a/weather_reporter/views.py b/weather_reporter/views.py index abe325e..4aa85fd 100644 --- a/weather_reporter/views.py +++ b/weather_reporter/views.py @@ -29,7 +29,7 @@ def weather_reporter_detail(request): 'agent': agent, 'user_requests': user_requests } - return render(request, 'detail.html', context) + return render(request, 'weather_reporter/detail.html', context) @method_decorator(csrf_exempt, name='dispatch') @@ -50,7 +50,7 @@ class WeatherReporterProcessView(View): if not request.user.has_sufficient_balance(agent.price): return JsonResponse({'error': 'Insufficient wallet balance'}, status=400) - # Create request object + # Create request object (no wallet deduction yet - only after successful processing) agent_request = WeatherReporterRequest.objects.create( user=request.user, agent=agent, @@ -60,13 +60,6 @@ class WeatherReporterProcessView(View): ) - # Deduct from wallet - request.user.deduct_balance( - agent.price, - f"Weather Reporter request for {data.get('location', 'unknown location')}", - 'weather-reporter' - ) - # Process request processor = WeatherReporterProcessor() result = processor.process_request( @@ -77,10 +70,14 @@ class WeatherReporterProcessView(View): ) + # Refresh user from database to get updated wallet balance + request.user.refresh_from_db() + return JsonResponse({ 'success': True, 'request_id': str(agent_request.id), - 'message': 'Weather Reporter request processed successfully' + 'message': 'Weather Reporter request processed successfully', + 'wallet_balance': float(request.user.wallet_balance) }) except BaseAgent.DoesNotExist: @@ -100,6 +97,9 @@ def weather_reporter_result(request, request_id): if hasattr(agent_request, 'response'): response = agent_request.response + # Refresh user to get current wallet balance + request.user.refresh_from_db() + return JsonResponse({ 'success': response.success, 'status': agent_request.status, @@ -111,7 +111,8 @@ def weather_reporter_result(request, request_id): 'formatted_report': getattr(response, 'formatted_report', None), 'processing_time': float(response.processing_time) if response.processing_time else None, - 'error_message': response.error_message + 'error_message': response.error_message, + 'wallet_balance': float(request.user.wallet_balance) }) else: return JsonResponse({