mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 10:13:00 +00:00
🚀 Implement scalable direct access architecture for 100+ agents
## Complete Scalable Architecture Implementation **Problem Solved**: CyberSec Career Navigator was broken (empty form → JotForm URL webhook) **Solution**: Built generic direct access architecture for unlimited external form agents ## New Architecture Features **🎯 Direct Access Agents (External Forms):** - Generic view functions work for ANY direct access agent - Database-driven routing via access_url_name/display_url_name fields - Template automatically detects and redirects to external forms - Zero code changes needed for new direct access agents **🔧 Webhook Agents (Dynamic Processing):** - Existing webhook agents work exactly as before (unchanged) - Dynamic form generation and N8N processing preserved - Zero impact on current functionality ## Files Added/Modified **Views** (): - `direct_access_handler()` - Generic payment & access processing - `direct_access_display()` - Generic external form display **URLs** (): - Generic routing: `<slug>/access/` and `<slug>/display/` - Works for any agent with access_url_name configured **Template** (): - Conditional logic: `{% if agent.access_url_name %}` - Direct access → "Start Consultation" button → External form - Webhook → Dynamic form (unchanged) **Migration** (): - Adds access_url_name and display_url_name fields to Agent model **Agent Config** (): - Updated to use generic direct access URLs ## Scalability Achievement **Ready for 100+ Agents:** - ✅ New webhook agent: Database record with empty access_url_name - ✅ New direct access agent: Database record with generic access_url_name - ✅ Zero code changes needed for new agents - ✅ Template automatically handles both types - ✅ Database-driven routing eliminates hardcoded URLs ## Expected Results **After Railway Deployment:** - CyberSec Career Navigator: "Start Consultation (FREE)" → Direct JotForm - All webhook agents: Continue working exactly as before - System ready for unlimited agents of both types 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
10f7e0d11e
commit
6fae2686c5
@ -34,8 +34,8 @@ class Command(BaseCommand):
|
|||||||
'fields': [] # Empty since we're using JotForm directly
|
'fields': [] # Empty since we're using JotForm directly
|
||||||
},
|
},
|
||||||
'webhook_url': 'https://agent.jotform.com/019865a942ab7fa5b5b743a5fd2abe09e345',
|
'webhook_url': 'https://agent.jotform.com/019865a942ab7fa5b5b743a5fd2abe09e345',
|
||||||
'access_url_name': '',
|
'access_url_name': 'agents:direct_access_handler',
|
||||||
'display_url_name': ''
|
'display_url_name': 'agents:direct_access_display'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
# Generated by Django 5.2.4 on 2025-08-04 10:45
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def check_and_add_fields(apps, schema_editor):
|
|
||||||
"""Add fields only if they don't already exist"""
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
|
|
||||||
# Check if columns already exist in the database
|
|
||||||
with schema_editor.connection.cursor() as cursor:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT column_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'agents_agent'
|
|
||||||
AND column_name IN ('access_url_name', 'display_url_name')
|
|
||||||
""")
|
|
||||||
existing_columns = [row[0] for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
# Add access_url_name if it doesn't exist
|
|
||||||
if 'access_url_name' not in existing_columns:
|
|
||||||
cursor.execute("""
|
|
||||||
ALTER TABLE agents_agent
|
|
||||||
ADD COLUMN access_url_name VARCHAR(100) DEFAULT '' NOT NULL
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Add display_url_name if it doesn't exist
|
|
||||||
if 'display_url_name' not in existing_columns:
|
|
||||||
cursor.execute("""
|
|
||||||
ALTER TABLE agents_agent
|
|
||||||
ADD COLUMN display_url_name VARCHAR(100) DEFAULT '' NOT NULL
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_check_and_add_fields(apps, schema_editor):
|
|
||||||
"""Remove fields if they exist"""
|
|
||||||
with schema_editor.connection.cursor() as cursor:
|
|
||||||
cursor.execute("ALTER TABLE agents_agent DROP COLUMN IF EXISTS access_url_name")
|
|
||||||
cursor.execute("ALTER TABLE agents_agent DROP COLUMN IF EXISTS display_url_name")
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("agents", "0004_add_message_limit"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(check_and_add_fields, reverse_check_and_add_fields),
|
|
||||||
]
|
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-04 17:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agents", "0005_auto_20250804_1105"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="agent",
|
||||||
|
name="access_url_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
help_text="URL name for direct access agents",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="agent",
|
||||||
|
name="display_url_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
help_text="URL name for agent display page",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="chatsession",
|
||||||
|
name="expires_at",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Session expiration time (30 minutes from last activity)",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -39,7 +39,42 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="widget-content">
|
<div class="widget-content">
|
||||||
<form id="agentForm" method="POST" enctype="multipart/form-data" data-agent-id="{{ agent.id }}">
|
{% if agent.access_url_name and agent.display_url_name %}
|
||||||
|
<!-- Direct Access Agent - External Form -->
|
||||||
|
<div class="direct-access-info">
|
||||||
|
<div class="section-container">
|
||||||
|
<h4 class="section-subtitle">{{ agent.category.icon }} {{ agent.name }} - External Consultation</h4>
|
||||||
|
<p style="color: #6b7280; margin-bottom: var(--spacing-lg);">
|
||||||
|
This consultation will redirect you to our specialized external form for personalized guidance.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
{% if agent.price == 0 %}
|
||||||
|
<a href="{% url 'agents:direct_access_handler' agent.slug %}" class="btn btn-primary btn-full">
|
||||||
|
{{ agent.category.icon }} Start {{ agent.name }} Consultation (FREE)
|
||||||
|
</a>
|
||||||
|
{% elif user.wallet_balance >= agent.price %}
|
||||||
|
<a href="{% url 'agents:direct_access_handler' agent.slug %}" class="btn btn-primary btn-full">
|
||||||
|
{{ agent.category.icon }} Start {{ agent.name }} Consultation ({{ agent.price }} AED)
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
|
||||||
|
Insufficient balance! You need {{ agent.price }} AED.
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
|
||||||
|
💰 Top Up Wallet
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
|
||||||
|
🔐 Login to Continue
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- Webhook Agent - Dynamic Form -->
|
||||||
|
<form id="agentForm" method="POST" enctype="multipart/form-data" data-agent-id="{{ agent.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Dynamic Form Fields -->
|
<!-- Dynamic Form Fields -->
|
||||||
@ -167,6 +202,7 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,10 @@ urlpatterns = [
|
|||||||
path('api/', views.agent_list, name='agent_list'),
|
path('api/', views.agent_list, name='agent_list'),
|
||||||
path('api/<slug:slug>/', views.agent_detail, name='agent_detail_api'),
|
path('api/<slug:slug>/', views.agent_detail, name='agent_detail_api'),
|
||||||
|
|
||||||
|
# Generic direct access routes (must be before agent detail)
|
||||||
|
path('<slug:slug>/access/', views.direct_access_handler, name='direct_access_handler'),
|
||||||
|
path('<slug:slug>/display/', views.direct_access_display, name='direct_access_display'),
|
||||||
|
|
||||||
# Agent detail page (must be last to avoid conflicts)
|
# Agent detail page (must be last to avoid conflicts)
|
||||||
path('<slug:slug>/', views.agent_detail_view, name='detail'),
|
path('<slug:slug>/', views.agent_detail_view, name='detail'),
|
||||||
]
|
]
|
||||||
@ -994,3 +994,62 @@ def export_chat_txt(chat_session, messages):
|
|||||||
response = HttpResponse(text_content, content_type='text/plain')
|
response = HttpResponse(text_content, content_type='text/plain')
|
||||||
response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.txt"'
|
response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.txt"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# Generic Direct Access Views for External Form Agents
|
||||||
|
@login_required
|
||||||
|
def direct_access_handler(request, slug):
|
||||||
|
"""
|
||||||
|
Generic handler for direct access agents (external forms like JotForm).
|
||||||
|
Handles payment processing and grants access to external form.
|
||||||
|
"""
|
||||||
|
agent = get_object_or_404(Agent, slug=slug, is_active=True)
|
||||||
|
|
||||||
|
# Verify this is a direct access agent
|
||||||
|
if not agent.access_url_name or not agent.display_url_name:
|
||||||
|
messages.error(request, 'This agent does not support direct access.')
|
||||||
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
|
# Handle payment for paid agents
|
||||||
|
if agent.price > 0:
|
||||||
|
user_balance = request.user.wallet_balance
|
||||||
|
if user_balance < agent.price:
|
||||||
|
messages.error(request, f'Insufficient balance. You need {agent.price} AED but have {user_balance} AED.')
|
||||||
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
|
# Process payment
|
||||||
|
try:
|
||||||
|
from wallet.models import WalletTransaction
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
amount=-agent.price,
|
||||||
|
type='agent_usage',
|
||||||
|
description=f'Payment for {agent.name}',
|
||||||
|
agent_slug=agent.slug
|
||||||
|
)
|
||||||
|
messages.success(request, f'Payment of {agent.price} AED processed successfully.')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, 'Payment processing failed. Please try again.')
|
||||||
|
return redirect('agents:agent_detail', slug=slug)
|
||||||
|
|
||||||
|
# Grant access - redirect to display page
|
||||||
|
messages.success(request, f'Access granted to {agent.name}. Redirecting to consultation form...')
|
||||||
|
return redirect('agents:direct_access_display', slug=slug)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def direct_access_display(request, slug):
|
||||||
|
"""
|
||||||
|
Generic display handler for direct access agents.
|
||||||
|
Shows external form (JotForm, Google Forms, etc.) in iframe or redirects directly.
|
||||||
|
"""
|
||||||
|
agent = get_object_or_404(Agent, slug=slug, is_active=True)
|
||||||
|
|
||||||
|
# Verify this is a direct access agent
|
||||||
|
if not agent.access_url_name or not agent.display_url_name:
|
||||||
|
messages.error(request, 'This agent does not support direct access.')
|
||||||
|
return redirect('agents:marketplace')
|
||||||
|
|
||||||
|
# For now, redirect directly to external form
|
||||||
|
# Future: Can render iframe template or custom display logic
|
||||||
|
return redirect(agent.webhook_url)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user