mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 07:52:58 +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
|
||||
},
|
||||
'webhook_url': 'https://agent.jotform.com/019865a942ab7fa5b5b743a5fd2abe09e345',
|
||||
'access_url_name': '',
|
||||
'display_url_name': ''
|
||||
'access_url_name': 'agents:direct_access_handler',
|
||||
'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>
|
||||
</div>
|
||||
<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 %}
|
||||
|
||||
<!-- Dynamic Form Fields -->
|
||||
@ -167,6 +202,7 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -27,6 +27,10 @@ urlpatterns = [
|
||||
path('api/', views.agent_list, name='agent_list'),
|
||||
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)
|
||||
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['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.txt"'
|
||||
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