Implement wallet deduction after success & fix display issues

🚀 Major improvements to agent system:

## Wallet Management 
- Move wallet deduction to AFTER successful processing (not before)
- Add real-time wallet balance updates in frontend
- Prevent users from losing money on failed requests
- Update both data_analyzer and weather_reporter processors

## Data Analyzer Agent 📊
- Fix N8N integration to handle array response format
- Add binary PDF file upload (multipart/form-data)
- Implement real-time AJAX results display below form
- Add wallet balance updates after successful processing
- Support continuous workflow with "Analyze Another File"

## Weather Reporter Agent 🌤️
- Update price from 2.5 AED to 2.0 AED
- Fix results display (was using page reload, now AJAX)
- Add real-time wallet balance updates
- Implement dynamic results rendering below form
- Add "Get Another Report" functionality

## Template System 🎨
- Update agent generator templates with correct wallet flow
- Add data-wallet-balance attributes for easy targeting
- Fix JavaScript querySelector errors
- Implement proper error handling and logging

## Documentation 📚
- Update CLAUDE.md with wallet best practices
- Add examples of current production agents
- Document required JavaScript functions
- Update pricing information and modern agent features
- Add gitignore entries for nextjs/ and netcop-ai-hub/

## Frontend JavaScript 💻
- Add updateWalletBalance() function for real-time updates
- Implement displayResults() for dynamic content rendering
- Add proper error handling with user-friendly messages
- Support continuous workflow without page refresh

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2025-07-10 16:34:03 +05:30
parent 1aac14f44b
commit 375b2b9d65
31 changed files with 2638 additions and 66 deletions

6
.gitignore vendored
View File

@ -225,4 +225,8 @@ ipython_config.py
*.pem
*.p12
*.pfx
secrets.jsonnetcop-ai-hub/
secrets.json
# NextJS frontend directory
nextjs/
netcop-ai-hub/

View File

@ -0,0 +1 @@
# {{ agent_name }} Agent App

View File

@ -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']

View File

@ -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'

View File

@ -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

View File

@ -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 }}'

View File

@ -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/<uuid:request_id>/', views.{{ agent_slug_underscore }}_result, name='result'),
]

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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}")

View File

@ -0,0 +1 @@
# Data Analysis Agent Agent App

19
data_analyzer/admin.py Normal file
View File

@ -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']

6
data_analyzer/apps.py Normal file
View File

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

View File

@ -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',
},
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

51
data_analyzer/models.py Normal file
View File

@ -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'

149
data_analyzer/processor.py Normal file
View File

@ -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}")

File diff suppressed because it is too large Load Diff

10
data_analyzer/urls.py Normal file
View File

@ -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/<uuid:request_id>/', views.data_analyzer_result, name='result'),
]

129
data_analyzer/views.py Normal file
View File

@ -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)

View File

@ -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
---

View File

@ -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
#### 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
<!-- Required data attributes for wallet balance -->
<span data-wallet-balance>{{ user.wallet_balance|floatformat:2 }} AED</span>
<div data-wallet-balance>{{ user.wallet_balance|floatformat:2 }} AED</div>
```

View File

@ -57,6 +57,7 @@ agent_pdf_analyzer/
├── migrations/
│ └── __init__.py
└── templates/
└── agent_pdf_analyzer/
└── detail.html
```
@ -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
<!-- agent_pdf_analyzer/templates/detail.html -->
<!-- agent_pdf_analyzer/templates/agent_pdf_analyzer/detail.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
@ -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)
"
```

View File

@ -48,6 +48,7 @@ INSTALLED_APPS = [
'core',
'agent_base',
'weather_reporter',
'data_analyzer',
]
MIDDLEWARE = [

View File

@ -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')),
]

View File

@ -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()

View File

@ -269,7 +269,7 @@
<a href="{% url 'core:marketplace' %}" style="color: #374151; text-decoration: none; font-weight: 500;">Marketplace</a>
{% if user.is_authenticated %}
<a href="{% url 'core:wallet' %}" style="color: #374151; text-decoration: none; font-weight: 500;">Wallet</a>
<span style="color: #6b7280;">{{ user.wallet_balance|floatformat:2 }} AED</span>
<span style="color: #6b7280;" data-wallet-balance>{{ user.wallet_balance|floatformat:2 }} AED</span>
{% else %}
<a href="{% url 'authentication:login' %}" style="color: #3b82f6; text-decoration: none; font-weight: 600;">Login</a>
{% endif %}
@ -415,7 +415,7 @@
<h3 class="section-title">💳 Your Wallet</h3>
<div style="margin-bottom: clamp(16px, 4vw, 20px);">
<div style="font-size: clamp(24px, 6vw, 28px); font-weight: 700; color: #1f2937;">
<div style="font-size: clamp(24px, 6vw, 28px); font-weight: 700; color: #1f2937;" data-wallet-balance>
{% 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 = `
<div class="weather-results">
<!-- Status Header -->
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
<div style="font-size: 24px;"></div>
<h3 style="font-size: 20px; font-weight: 600; color: #1f2937; margin: 0;">
Weather Report Results
</h3>
<div style="
background: #10b981;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
margin-left: auto;
">
✅ Complete
</div>
</div>
<!-- Weather Content -->
<div style="
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
">
<h4 style="
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-bottom: 16px;
">
🌤️ Weather Information
</h4>
<div style="
white-space: pre-line;
line-height: 1.7;
color: #374151;
font-size: 15px;
" id="weatherContent">
${result.formatted_report || result.weather_data || 'Weather data processed successfully'}
</div>
${result.temperature ? `
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: #1f2937;">${result.temperature}°C</div>
<div style="font-size: 14px; color: #6b7280;">Temperature</div>
</div>
${result.humidity ? `
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: #1f2937;">${result.humidity}%</div>
<div style="font-size: 14px; color: #6b7280;">Humidity</div>
</div>
` : ''}
${result.wind_speed ? `
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 700; color: #1f2937;">${result.wind_speed} m/s</div>
<div style="font-size: 14px; color: #6b7280;">Wind Speed</div>
</div>
` : ''}
</div>
</div>
` : ''}
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
<h5 style="font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">
📄 Processing Information
</h5>
<div style="font-size: 14px; color: #6b7280;">
<strong>Processing Time:</strong> ${result.processing_time ? result.processing_time.toFixed(2) + 's' : 'N/A'}<br>
<strong>Status:</strong> ${result.status}<br>
<strong>Completed:</strong> ${new Date().toLocaleString()}
</div>
</div>
</div>
<!-- Download/Copy Actions -->
<div style="
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
flex-wrap: wrap;
">
<button
onclick="copyWeatherReport()"
class="btn btn-primary"
style="flex: 1; min-width: 120px;"
>
📋 Copy Report
</button>
<button
onclick="downloadWeatherReport()"
class="btn"
style="
flex: 1;
min-width: 120px;
background: white;
color: #374151;
border: 2px solid #e5e7eb;
"
>
💾 Download Report
</button>
<button
onclick="getAnotherReport()"
class="btn"
style="
flex: 1;
min-width: 120px;
background: #10b981;
color: white;
border: 2px solid #10b981;
"
>
🌤️ Get Another Report
</button>
</div>
</div>
`;
// 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');
}
</script>
</body>
</html>

View File

@ -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({