From 6fae2686c5e60ad3a7d43f4e1d9aba9bc9940c10 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 Aug 2025 22:46:00 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Implement=20scalable=20direct=20?= =?UTF-8?q?access=20architecture=20for=20100+=20agents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: `/access/` and `/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 --- .../commands/create_cybersec_career_agent.py | 4 +- agents/migrations/0005_auto_20250804_1045.py | 50 ---------------- ...rl_name_agent_display_url_name_and_more.py | 42 +++++++++++++ agents/templates/agents/agent_detail.html | 38 +++++++++++- agents/urls.py | 4 ++ agents/views.py | 59 +++++++++++++++++++ 6 files changed, 144 insertions(+), 53 deletions(-) delete mode 100644 agents/migrations/0005_auto_20250804_1045.py create mode 100644 agents/migrations/0006_agent_access_url_name_agent_display_url_name_and_more.py diff --git a/agents/management/commands/create_cybersec_career_agent.py b/agents/management/commands/create_cybersec_career_agent.py index 6f3f3d4..b154741 100644 --- a/agents/management/commands/create_cybersec_career_agent.py +++ b/agents/management/commands/create_cybersec_career_agent.py @@ -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' } ) diff --git a/agents/migrations/0005_auto_20250804_1045.py b/agents/migrations/0005_auto_20250804_1045.py deleted file mode 100644 index 06fa14f..0000000 --- a/agents/migrations/0005_auto_20250804_1045.py +++ /dev/null @@ -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), - ] diff --git a/agents/migrations/0006_agent_access_url_name_agent_display_url_name_and_more.py b/agents/migrations/0006_agent_access_url_name_agent_display_url_name_and_more.py new file mode 100644 index 0000000..78c5752 --- /dev/null +++ b/agents/migrations/0006_agent_access_url_name_agent_display_url_name_and_more.py @@ -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, + ), + ), + ] diff --git a/agents/templates/agents/agent_detail.html b/agents/templates/agents/agent_detail.html index a68cb44..82eae99 100644 --- a/agents/templates/agents/agent_detail.html +++ b/agents/templates/agents/agent_detail.html @@ -39,7 +39,42 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}');
-
+ {% if agent.access_url_name and agent.display_url_name %} + +
+
+

{{ agent.category.icon }} {{ agent.name }} - External Consultation

+

+ This consultation will redirect you to our specialized external form for personalized guidance. +

+ + {% if user.is_authenticated %} + {% if agent.price == 0 %} + + {{ agent.category.icon }} Start {{ agent.name }} Consultation (FREE) + + {% elif user.wallet_balance >= agent.price %} + + {{ agent.category.icon }} Start {{ agent.name }} Consultation ({{ agent.price }} AED) + + {% else %} +
+ Insufficient balance! You need {{ agent.price }} AED. +
+ + 💰 Top Up Wallet + + {% endif %} + {% else %} + + 🔐 Login to Continue + + {% endif %} +
+
+ {% else %} + + {% csrf_token %} @@ -167,6 +202,7 @@ document.body.setAttribute('data-user-balance', '{{ user.wallet_balance }}'); {% endif %}
+ {% endif %} diff --git a/agents/urls.py b/agents/urls.py index 381d020..96e490f 100644 --- a/agents/urls.py +++ b/agents/urls.py @@ -27,6 +27,10 @@ urlpatterns = [ path('api/', views.agent_list, name='agent_list'), path('api//', views.agent_detail, name='agent_detail_api'), + # Generic direct access routes (must be before agent detail) + path('/access/', views.direct_access_handler, name='direct_access_handler'), + path('/display/', views.direct_access_display, name='direct_access_display'), + # Agent detail page (must be last to avoid conflicts) path('/', views.agent_detail_view, name='detail'), ] \ No newline at end of file diff --git a/agents/views.py b/agents/views.py index fcdc3db..5336ad4 100644 --- a/agents/views.py +++ b/agents/views.py @@ -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)