🚀 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:
Claude 2025-08-04 22:46:00 +05:30
parent 10f7e0d11e
commit 6fae2686c5
6 changed files with 144 additions and 53 deletions

View File

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

View File

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

View File

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

View File

@ -39,6 +39,41 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
</h3> </h3>
</div> </div>
<div class="widget-content"> <div class="widget-content">
{% 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 }}"> <form id="agentForm" method="POST" enctype="multipart/form-data" data-agent-id="{{ agent.id }}">
{% csrf_token %} {% csrf_token %}
@ -167,6 +202,7 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
{% endif %} {% endif %}
</div> </div>
</form> </form>
{% endif %}
</div> </div>
</div> </div>

View File

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

View File

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