mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 18:12:58 +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
|
*.pem
|
||||||
*.p12
|
*.p12
|
||||||
*.pfx
|
*.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
|
# Agent Setup Checklist
|
||||||
## Steps to Complete After Running `create_agent` Command
|
## 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
|
```bash
|
||||||
# Your agent templates should be in:
|
# Create the template directory and file:
|
||||||
agent_[name]/templates/detail.html
|
mkdir -p [agent_name]/templates/
|
||||||
|
|
||||||
# Example for PDF Analyzer:
|
|
||||||
agent_pdf_analyzer/templates/detail.html
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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**
|
## 📝 **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`
|
- [ ] **Settings:** Add agent to `INSTALLED_APPS`
|
||||||
- [ ] **URLs:** Add URL pattern to `netcop_hub/urls.py`
|
- [ ] **URLs:** Add URL pattern to `netcop_hub/urls.py`
|
||||||
- [ ] **Database:** Run `makemigrations` and `migrate`
|
- [ ] **Database:** Run `makemigrations` and `migrate`
|
||||||
- [ ] **Marketplace:** Create `BaseAgent` entry
|
- [ ] **Marketplace:** Create `BaseAgent` entry (done automatically)
|
||||||
- [ ] **Environment:** Add API keys to `.env`
|
- [ ] **Environment:** Add API keys to `.env`
|
||||||
|
- [ ] **Template:** Create and customize `detail.html` template
|
||||||
- [ ] **Test:** Verify agent works end-to-end
|
- [ ] **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)
|
├── 📁 core/ # Main app (homepage, marketplace, wallet)
|
||||||
├── 📁 wallet/ # Payment and transaction system
|
├── 📁 wallet/ # Payment and transaction system
|
||||||
├── 📁 weather_reporter/ # Example individual agent app
|
├── 📁 weather_reporter/ # Example individual agent app
|
||||||
│ └── templates/ # Agent-specific templates
|
│ └── templates/ # Agent-specific templates (namespaced)
|
||||||
|
│ └── weather_reporter/
|
||||||
|
│ └── detail.html
|
||||||
├── 📁 templates/ # Global templates (core, auth)
|
├── 📁 templates/ # Global templates (core, auth)
|
||||||
├── 📁 static/ # Static assets (CSS, JS, images)
|
├── 📁 static/ # Static assets (CSS, JS, images)
|
||||||
├── 📁 media/ # User-uploaded files
|
├── 📁 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
|
- Custom User model with wallet balance functionality
|
||||||
- Stripe integration for payments (`wallet/stripe_handler.py`)
|
- Stripe integration for payments (`wallet/stripe_handler.py`)
|
||||||
- Transaction tracking via `WalletTransaction` model
|
- 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
|
## Essential Commands
|
||||||
|
|
||||||
@ -134,8 +137,8 @@ Each agent requires webhook URLs in format:
|
|||||||
|
|
||||||
## Agent Creation System (Automated)
|
## Agent Creation System (Automated)
|
||||||
|
|
||||||
### Automated Agent Creation Command
|
### Automated Agent Creation Command (✅ FULLY FUNCTIONAL)
|
||||||
The project features a sophisticated automated agent creation system via the `create_agent` management command:
|
The project features a sophisticated automated agent creation system via the `create_agent` management command with complete Django template generation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create webhook-based agent (N8N integration)
|
# 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
|
- **StandardAPIProcessor**: Handles direct API calls with flexible authentication methods
|
||||||
- **WebhookFormatDetector**: Utility to test and detect webhook formats
|
- **WebhookFormatDetector**: Utility to test and detect webhook formats
|
||||||
|
|
||||||
#### Template-Based Code Generation
|
#### Template-Based Code Generation (✅ COMPLETE)
|
||||||
The system uses Django templates to generate complete agent apps:
|
The system uses Django templates in `agent_base/templates/agent_generator/` to generate complete agent apps:
|
||||||
|
|
||||||
**Template Files:**
|
**Available Template Files:**
|
||||||
- `webhook_models.py` / `api_models.py`: Models with custom fields
|
- `api_models.py` / `webhook_models.py`: Database models with custom fields
|
||||||
- `webhook_processor.py` / `api_processor.py`: Processor classes
|
- `api_processor.py` / `webhook_processor.py` / `weather_api_processor.py`: Processor classes
|
||||||
- `views.py`: Django views with authentication and wallet integration
|
- `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
|
- `admin.py`: Django admin configuration
|
||||||
- `apps.py`: Django app configuration
|
- `apps.py`: Django app configuration
|
||||||
|
- `__init__.py`: App initialization
|
||||||
|
|
||||||
#### Supported Agent Types
|
#### Supported Agent Types
|
||||||
|
|
||||||
@ -184,26 +188,39 @@ The system uses Django templates to generate complete agent apps:
|
|||||||
- GET/POST request support
|
- GET/POST request support
|
||||||
- Response parsing and formatting
|
- Response parsing and formatting
|
||||||
|
|
||||||
#### Weather Reporter Example
|
#### Example Agents (Production Ready)
|
||||||
The system includes a complete Weather Reporter agent example:
|
|
||||||
- **API Integration**: OpenWeatherMap API
|
**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
|
- **Custom Fields**: location, report_type, temperature, humidity, wind_speed
|
||||||
- **Formatted Reports**: Both current and detailed weather reports
|
- **Formatted Reports**: Both current and detailed weather reports
|
||||||
|
- **Real-time Results**: Dynamic display below form
|
||||||
- **Error Handling**: API failures and invalid locations
|
- **Error Handling**: API failures and invalid locations
|
||||||
|
|
||||||
### Management Commands
|
### Management Commands
|
||||||
|
|
||||||
#### create_agent
|
#### create_agent (✅ READY TO USE)
|
||||||
Generates complete agent apps with:
|
Generates complete agent apps with:
|
||||||
- Database models and migrations
|
- Database models and migrations
|
||||||
- Processor classes
|
- Processor classes (API/webhook/weather-specific)
|
||||||
- Django views with authentication
|
- Django views with authentication and wallet integration
|
||||||
- URL routing
|
- URL routing with proper namespacing
|
||||||
- Admin interface
|
- Admin interface with list views
|
||||||
- Custom field definitions based on agent type
|
- Custom field definitions based on agent type
|
||||||
|
- Simplified template structure: `agent_name/templates/detail.html`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python manage.py create_agent --help
|
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
|
#### test_webhook
|
||||||
@ -308,7 +325,7 @@ Each agent app has:
|
|||||||
Templates follow clean Django app structure:
|
Templates follow clean Django app structure:
|
||||||
- `templates/core/`: Homepage, marketplace, wallet (global templates)
|
- `templates/core/`: Homepage, marketplace, wallet (global templates)
|
||||||
- `templates/authentication/`: Login, registration (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
|
- `docs/`: All documentation and guides
|
||||||
- `tests/`: All test files
|
- `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
|
2. Run `python manage.py populate_base_agents` to update database
|
||||||
3. Pricing is enforced in `BaseAgentView.post()` method
|
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)
|
## Current Architecture (Clean & Modern)
|
||||||
|
|
||||||
The project uses a clean, modular individual agent architecture:
|
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
|
- **Organized project structure**: Documentation in `docs/`, tests in `tests/`, clean root directory
|
||||||
- **BaseAgent catalog system**: Centralized marketplace with individual agent implementations
|
- **BaseAgent catalog system**: Centralized marketplace with individual agent implementations
|
||||||
- **Modular processors**: Each agent has its own processor for API/webhook integration
|
- **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
|
### Best Practices
|
||||||
- All new agents should follow the individual app architecture
|
|
||||||
- Templates should be placed within the agent app, not in global templates
|
#### Agent Development Standards
|
||||||
- Use the `create_agent` command for automated setup, then follow the setup checklist
|
- **Individual App Architecture**: Each agent is a separate Django app
|
||||||
- Keep root directory clean - use `docs/` and `tests/` folders for organization
|
- **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,6 +57,7 @@ agent_pdf_analyzer/
|
|||||||
├── migrations/
|
├── migrations/
|
||||||
│ └── __init__.py
|
│ └── __init__.py
|
||||||
└── templates/
|
└── templates/
|
||||||
|
└── agent_pdf_analyzer/
|
||||||
└── detail.html
|
└── detail.html
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -264,7 +265,7 @@ def pdf_analyzer_detail(request):
|
|||||||
'agent': agent,
|
'agent': agent,
|
||||||
'user_requests': user_requests
|
'user_requests': user_requests
|
||||||
}
|
}
|
||||||
return render(request, 'detail.html', context)
|
return render(request, 'agent_pdf_analyzer/detail.html', context)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 Process View
|
### 4.2 Process View
|
||||||
@ -402,12 +403,12 @@ urlpatterns = [
|
|||||||
|
|
||||||
### 6.1 Create Template Directory
|
### 6.1 Create Template Directory
|
||||||
```bash
|
```bash
|
||||||
mkdir -p agent_pdf_analyzer/templates/
|
mkdir -p agent_pdf_analyzer/templates/agent_pdf_analyzer/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.2 Detail Template
|
### 6.2 Detail Template
|
||||||
```html
|
```html
|
||||||
<!-- agent_pdf_analyzer/templates/detail.html -->
|
<!-- agent_pdf_analyzer/templates/agent_pdf_analyzer/detail.html -->
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -733,10 +734,10 @@ python manage.py shell
|
|||||||
**Fix**: Ensure template is in correct location within the agent app:
|
**Fix**: Ensure template is in correct location within the agent app:
|
||||||
```bash
|
```bash
|
||||||
# Correct location:
|
# Correct location:
|
||||||
agent_[name]/templates/detail.html
|
agent_[name]/templates/agent_[name]/detail.html
|
||||||
|
|
||||||
# Example:
|
# Example:
|
||||||
agent_pdf_analyzer/templates/detail.html
|
agent_pdf_analyzer/templates/agent_pdf_analyzer/detail.html
|
||||||
|
|
||||||
# NOT in global templates folder
|
# NOT in global templates folder
|
||||||
# Restart Django server after moving templates
|
# Restart Django server after moving templates
|
||||||
@ -746,7 +747,7 @@ agent_pdf_analyzer/templates/detail.html
|
|||||||
```bash
|
```bash
|
||||||
python manage.py shell -c "
|
python manage.py shell -c "
|
||||||
from django.template.loader import get_template
|
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)
|
print('✅ Template found:', template.origin.name)
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|||||||
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
|||||||
'core',
|
'core',
|
||||||
'agent_base',
|
'agent_base',
|
||||||
'weather_reporter',
|
'weather_reporter',
|
||||||
|
'data_analyzer',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@ -23,6 +23,7 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('auth/', include('authentication.urls')),
|
path('auth/', include('authentication.urls')),
|
||||||
path('agents/weather-reporter/', include('weather_reporter.urls')),
|
path('agents/weather-reporter/', include('weather_reporter.urls')),
|
||||||
|
path('agents/data-analyzer/', include('data_analyzer.urls')),
|
||||||
path('', include('core.urls')),
|
path('', include('core.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -91,10 +91,13 @@ Weather data provided by OpenWeatherMap"""
|
|||||||
# Format report
|
# Format report
|
||||||
formatted_report = self.format_weather_report(weather_data, request_obj.report_type)
|
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
|
# Create response object
|
||||||
response_obj = WeatherReporterResponse.objects.create(
|
response_obj = WeatherReporterResponse.objects.create(
|
||||||
request=request_obj,
|
request=request_obj,
|
||||||
success=response_data.get('success', True),
|
success=success,
|
||||||
processing_time=response_data.get('processing_time', 0),
|
processing_time=response_data.get('processing_time', 0),
|
||||||
weather_data=weather_data,
|
weather_data=weather_data,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
@ -104,8 +107,17 @@ Weather data provided by OpenWeatherMap"""
|
|||||||
formatted_report=formatted_report,
|
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
|
# Update request as completed
|
||||||
request_obj.status = 'completed'
|
request_obj.status = 'completed' if success else 'failed'
|
||||||
request_obj.processed_at = timezone.now()
|
request_obj.processed_at = timezone.now()
|
||||||
request_obj.save()
|
request_obj.save()
|
||||||
|
|
||||||
|
|||||||
@ -269,7 +269,7 @@
|
|||||||
<a href="{% url 'core:marketplace' %}" style="color: #374151; text-decoration: none; font-weight: 500;">Marketplace</a>
|
<a href="{% url 'core:marketplace' %}" style="color: #374151; text-decoration: none; font-weight: 500;">Marketplace</a>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="{% url 'core:wallet' %}" style="color: #374151; text-decoration: none; font-weight: 500;">Wallet</a>
|
<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 %}
|
{% else %}
|
||||||
<a href="{% url 'authentication:login' %}" style="color: #3b82f6; text-decoration: none; font-weight: 600;">Login</a>
|
<a href="{% url 'authentication:login' %}" style="color: #3b82f6; text-decoration: none; font-weight: 600;">Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -415,7 +415,7 @@
|
|||||||
<h3 class="section-title">💳 Your Wallet</h3>
|
<h3 class="section-title">💳 Your Wallet</h3>
|
||||||
|
|
||||||
<div style="margin-bottom: clamp(16px, 4vw, 20px);">
|
<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 %}
|
{% if user.is_authenticated %}
|
||||||
{{ user.wallet_balance|floatformat:2 }} AED
|
{{ user.wallet_balance|floatformat:2 }} AED
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -795,8 +795,8 @@
|
|||||||
return;
|
return;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
// Check wallet balance
|
// Check wallet balance (use current balance if updated, otherwise template value)
|
||||||
const balance = {{ user.wallet_balance|default:0 }};
|
const balance = window.currentWalletBalance !== undefined ? window.currentWalletBalance : {{ user.wallet_balance|default:0 }};
|
||||||
if (balance < {{ agent.price }}) {
|
if (balance < {{ agent.price }}) {
|
||||||
showToast('Insufficient balance! You need {{ agent.price }} AED.', 'error');
|
showToast('Insufficient balance! You need {{ agent.price }} AED.', 'error');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -836,6 +836,11 @@
|
|||||||
showToast('Weather request submitted successfully!', 'success');
|
showToast('Weather request submitted successfully!', 'success');
|
||||||
showProcessingStatus('Processing weather data...');
|
showProcessingStatus('Processing weather data...');
|
||||||
|
|
||||||
|
// Update wallet balance if provided
|
||||||
|
if (data.wallet_balance !== undefined) {
|
||||||
|
updateWalletBalance(data.wallet_balance);
|
||||||
|
}
|
||||||
|
|
||||||
// Poll for results
|
// Poll for results
|
||||||
pollForResults(data.request_id);
|
pollForResults(data.request_id);
|
||||||
} else {
|
} else {
|
||||||
@ -898,12 +903,20 @@
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.status === 'completed') {
|
if (data.success && data.status === 'completed') {
|
||||||
// Success - reload page to show results
|
// Update wallet balance after successful completion
|
||||||
showToast('Weather report generated successfully!', 'success');
|
if (data.wallet_balance !== undefined) {
|
||||||
setTimeout(() => {
|
updateWalletBalance(data.wallet_balance);
|
||||||
window.location.reload();
|
}
|
||||||
}, 1000);
|
|
||||||
|
// Display results without page reload
|
||||||
|
displayWeatherResults(data);
|
||||||
|
hideProcessingStatus();
|
||||||
|
resetForm();
|
||||||
|
showToast('Weather report completed and payment processed!', 'success');
|
||||||
} else if (data.status === 'failed') {
|
} else if (data.status === 'failed') {
|
||||||
|
hideProcessingStatus();
|
||||||
|
resetForm();
|
||||||
|
showToast('Weather request failed - no charge applied', 'error');
|
||||||
throw new Error(data.error_message || 'Weather request failed');
|
throw new Error(data.error_message || 'Weather request failed');
|
||||||
} else {
|
} else {
|
||||||
// Still processing - poll again
|
// Still processing - poll again
|
||||||
@ -928,6 +941,201 @@
|
|||||||
submitBtn.innerHTML = '🌤️ Get Weather Report ({{ agent.price }} AED)';
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -29,7 +29,7 @@ def weather_reporter_detail(request):
|
|||||||
'agent': agent,
|
'agent': agent,
|
||||||
'user_requests': user_requests
|
'user_requests': user_requests
|
||||||
}
|
}
|
||||||
return render(request, 'detail.html', context)
|
return render(request, 'weather_reporter/detail.html', context)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
@ -50,7 +50,7 @@ class WeatherReporterProcessView(View):
|
|||||||
if not request.user.has_sufficient_balance(agent.price):
|
if not request.user.has_sufficient_balance(agent.price):
|
||||||
return JsonResponse({'error': 'Insufficient wallet balance'}, status=400)
|
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(
|
agent_request = WeatherReporterRequest.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
agent=agent,
|
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
|
# Process request
|
||||||
processor = WeatherReporterProcessor()
|
processor = WeatherReporterProcessor()
|
||||||
result = processor.process_request(
|
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({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'request_id': str(agent_request.id),
|
'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:
|
except BaseAgent.DoesNotExist:
|
||||||
@ -100,6 +97,9 @@ def weather_reporter_result(request, request_id):
|
|||||||
|
|
||||||
if hasattr(agent_request, 'response'):
|
if hasattr(agent_request, 'response'):
|
||||||
response = agent_request.response
|
response = agent_request.response
|
||||||
|
# Refresh user to get current wallet balance
|
||||||
|
request.user.refresh_from_db()
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': response.success,
|
'success': response.success,
|
||||||
'status': agent_request.status,
|
'status': agent_request.status,
|
||||||
@ -111,7 +111,8 @@ def weather_reporter_result(request, request_id):
|
|||||||
'formatted_report': getattr(response, 'formatted_report', None),
|
'formatted_report': getattr(response, 'formatted_report', None),
|
||||||
|
|
||||||
'processing_time': float(response.processing_time) if response.processing_time else 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:
|
else:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user