mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 22:32:59 +00:00
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:
parent
1aac14f44b
commit
375b2b9d65
6
.gitignore
vendored
6
.gitignore
vendored
@ -225,4 +225,8 @@ ipython_config.py
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
secrets.jsonnetcop-ai-hub/
|
||||
secrets.json
|
||||
|
||||
# NextJS frontend directory
|
||||
nextjs/
|
||||
netcop-ai-hub/
|
||||
|
||||
1
agent_base/templates/agent_generator/__init__.py
Normal file
1
agent_base/templates/agent_generator/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# {{ agent_name }} Agent App
|
||||
19
agent_base/templates/agent_generator/admin.py
Normal file
19
agent_base/templates/agent_generator/admin.py
Normal 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']
|
||||
35
agent_base/templates/agent_generator/api_models.py
Normal file
35
agent_base/templates/agent_generator/api_models.py
Normal 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'
|
||||
103
agent_base/templates/agent_generator/api_processor.py
Normal file
103
agent_base/templates/agent_generator/api_processor.py
Normal 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
|
||||
6
agent_base/templates/agent_generator/apps.py
Normal file
6
agent_base/templates/agent_generator/apps.py
Normal 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 }}'
|
||||
10
agent_base/templates/agent_generator/urls.py
Normal file
10
agent_base/templates/agent_generator/urls.py
Normal 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'),
|
||||
]
|
||||
123
agent_base/templates/agent_generator/views.py
Normal file
123
agent_base/templates/agent_generator/views.py
Normal 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)
|
||||
140
agent_base/templates/agent_generator/weather_api_processor.py
Normal file
140
agent_base/templates/agent_generator/weather_api_processor.py
Normal 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
|
||||
35
agent_base/templates/agent_generator/webhook_models.py
Normal file
35
agent_base/templates/agent_generator/webhook_models.py
Normal 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'
|
||||
70
agent_base/templates/agent_generator/webhook_processor.py
Normal file
70
agent_base/templates/agent_generator/webhook_processor.py
Normal 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}")
|
||||
1
data_analyzer/__init__.py
Normal file
1
data_analyzer/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Data Analysis Agent Agent App
|
||||
19
data_analyzer/admin.py
Normal file
19
data_analyzer/admin.py
Normal 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
6
data_analyzer/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DataAnalysisAgentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'data_analyzer'
|
||||
58
data_analyzer/migrations/0001_initial.py
Normal file
58
data_analyzer/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
47
data_analyzer/migrations/0002_auto_20250710_0431.py
Normal file
47
data_analyzer/migrations/0002_auto_20250710_0431.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
0
data_analyzer/migrations/__init__.py
Normal file
0
data_analyzer/migrations/__init__.py
Normal file
51
data_analyzer/models.py
Normal file
51
data_analyzer/models.py
Normal 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
149
data_analyzer/processor.py
Normal 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}")
|
||||
1138
data_analyzer/templates/data_analyzer/detail.html
Normal file
1138
data_analyzer/templates/data_analyzer/detail.html
Normal file
File diff suppressed because it is too large
Load Diff
10
data_analyzer/urls.py
Normal file
10
data_analyzer/urls.py
Normal 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
129
data_analyzer/views.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
200
docs/CLAUDE.md
200
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
|
||||
|
||||
#### 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>
|
||||
```
|
||||
@ -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
|
||||
<!-- 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)
|
||||
"
|
||||
```
|
||||
|
||||
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
||||
'core',
|
||||
'agent_base',
|
||||
'weather_reporter',
|
||||
'data_analyzer',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@ -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')),
|
||||
]
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user