Compare commits

...

6 Commits

Author SHA1 Message Date
Flatlogic Bot
a7f729c130 v9 2026-02-07 19:44:36 +00:00
Flatlogic Bot
1bd025b08b v8 2026-02-07 19:09:40 +00:00
Flatlogic Bot
26e52deb33 v7 2026-02-07 18:36:21 +00:00
Flatlogic Bot
84f67bc019 v4 2026-02-07 18:16:19 +00:00
Flatlogic Bot
5d275d310a v3 2026-02-07 18:05:13 +00:00
Flatlogic Bot
6c3327bd3f v1 2026-02-07 17:19:22 +00:00
23 changed files with 897 additions and 217 deletions

Binary file not shown.

Binary file not shown.

View File

@ -111,7 +111,6 @@ def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] =
return initial return initial
def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Perform a raw request to the AI proxy.""" """Perform a raw request to the AI proxy."""
cfg = _config() cfg = _config()
@ -144,7 +143,7 @@ def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict
headers: Dict[str, str] = { headers: Dict[str, str] = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
cfg["project_header"]: project_uuid, cfg["project_header"].strip(): project_uuid,
} }
extra_headers = options.get("headers") extra_headers = options.get("headers")
if isinstance(extra_headers, Iterable): if isinstance(extra_headers, Iterable):
@ -156,7 +155,6 @@ def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict
body = json.dumps(payload, ensure_ascii=False).encode("utf-8") body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
return _http_request(url, "POST", body, headers, timeout, verify_tls) return _http_request(url, "POST", body, headers, timeout, verify_tls)
def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Fetch status for a queued AI request.""" """Fetch status for a queued AI request."""
cfg = _config() cfg = _config()
@ -179,7 +177,7 @@ def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -
headers: Dict[str, str] = { headers: Dict[str, str] = {
"Accept": "application/json", "Accept": "application/json",
cfg["project_header"]: project_uuid, cfg["project_header"].strip(): project_uuid,
} }
extra_headers = options.get("headers") extra_headers = options.get("headers")
if isinstance(extra_headers, Iterable): if isinstance(extra_headers, Iterable):
@ -190,7 +188,6 @@ def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -
return _http_request(url, "GET", None, headers, timeout, verify_tls) return _http_request(url, "GET", None, headers, timeout, verify_tls)
def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Poll status endpoint until the request is complete or timed out.""" """Poll status endpoint until the request is complete or timed out."""
options = options or {} options = options or {}
@ -236,12 +233,10 @@ def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None)
} }
time.sleep(interval) time.sleep(interval)
def extract_text(response: Dict[str, Any]) -> str: def extract_text(response: Dict[str, Any]) -> str:
"""Public helper to extract plain text from a Responses payload.""" """Public helper to extract plain text from a Responses payload."""
return _extract_text(response) return _extract_text(response)
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Attempt to decode JSON emitted by the model (handles markdown fences).""" """Attempt to decode JSON emitted by the model (handles markdown fences)."""
text = _extract_text(response) text = _extract_text(response)
@ -294,7 +289,6 @@ def _extract_text(response: Dict[str, Any]) -> str:
return payload return payload
return "" return ""
def _config() -> Dict[str, Any]: def _config() -> Dict[str, Any]:
global _CONFIG_CACHE # noqa: PLW0603 global _CONFIG_CACHE # noqa: PLW0603
if _CONFIG_CACHE is not None: if _CONFIG_CACHE is not None:
@ -320,7 +314,6 @@ def _config() -> Dict[str, Any]:
} }
return _CONFIG_CACHE return _CONFIG_CACHE
def _build_url(path: str, base_url: str) -> str: def _build_url(path: str, base_url: str) -> str:
trimmed = path.strip() trimmed = path.strip()
if trimmed.startswith("http://") or trimmed.startswith("https://"): if trimmed.startswith("http://") or trimmed.startswith("https://"):
@ -329,7 +322,6 @@ def _build_url(path: str, base_url: str) -> str:
return f"{base_url}{trimmed}" return f"{base_url}{trimmed}"
return f"{base_url}/{trimmed}" return f"{base_url}/{trimmed}"
def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str: def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
base_path = (cfg.get("responses_path") or "").rstrip("/") base_path = (cfg.get("responses_path") or "").rstrip("/")
if not base_path: if not base_path:
@ -338,13 +330,17 @@ def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
base_path = f"{base_path}/ai-request" base_path = f"{base_path}/ai-request"
return f"{base_path}/{ai_request_id}/status" return f"{base_path}/{ai_request_id}/status"
def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str], def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str],
timeout: int, verify_tls: bool) -> Dict[str, Any]: timeout: int, verify_tls: bool) -> Dict[str, Any]:
""" """
Shared HTTP helper for GET/POST requests. Shared HTTP helper for GET/POST requests.
""" """
req = urlrequest.Request(url, data=body, method=method.upper()) req = urlrequest.Request(url, data=body, method=method.upper())
# Use a standard User-Agent to avoid being blocked by Cloudflare
if "User-Agent" not in headers:
headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
for name, value in headers.items(): for name, value in headers.items():
req.add_header(name, value) req.add_header(name, value)
@ -395,7 +391,6 @@ def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[st
"response": decoded if decoded is not None else response_body, "response": decoded if decoded is not None else response_body,
} }
def _ensure_env_loaded() -> None: def _ensure_env_loaded() -> None:
"""Populate os.environ from executor/.env if variables are missing.""" """Populate os.environ from executor/.env if variables are missing."""
if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"): if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"):
@ -413,7 +408,7 @@ def _ensure_env_loaded() -> None:
continue continue
key, value = stripped.split("=", 1) key, value = stripped.split("=", 1)
key = key.strip() key = key.strip()
value = value.strip().strip('\'"') value = value.strip().strip('"')
if key and not os.getenv(key): if key and not os.getenv(key):
os.environ[key] = value os.environ[key] = value
except OSError: except OSError:

Binary file not shown.

View File

@ -1,3 +1,26 @@
from django.contrib import admin from django.contrib import admin
from .models import Fanpage, Flow, Node, Edge, ChatSession, MessageLog
# Register your models here. @admin.register(Fanpage)
class FanpageAdmin(admin.ModelAdmin):
list_display = ('name', 'page_id', 'is_active', 'created_at')
@admin.register(Flow)
class FlowAdmin(admin.ModelAdmin):
list_display = ('name', 'fanpage', 'is_default', 'created_at')
@admin.register(Node)
class NodeAdmin(admin.ModelAdmin):
list_display = ('name', 'flow', 'node_type', 'is_start_node')
@admin.register(Edge)
class EdgeAdmin(admin.ModelAdmin):
list_display = ('source_node', 'target_node', 'condition')
@admin.register(ChatSession)
class ChatSessionAdmin(admin.ModelAdmin):
list_display = ('psid', 'fanpage', 'current_node', 'updated_at')
@admin.register(MessageLog)
class MessageLogAdmin(admin.ModelAdmin):
list_display = ('session', 'sender_type', 'timestamp')

View File

@ -0,0 +1,87 @@
# Generated by Django 5.2.7 on 2026-02-07 17:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Fanpage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('page_id', models.CharField(max_length=255, unique=True)),
('access_token', models.TextField()),
('verify_token', models.CharField(help_text='Token for Facebook Webhook verification', max_length=255)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='ChatSession',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('psid', models.CharField(help_text='Facebook Page Scoped ID of the user', max_length=255)),
('updated_at', models.DateTimeField(auto_now=True)),
('fanpage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.fanpage')),
],
),
migrations.CreateModel(
name='Flow',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('is_default', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('fanpage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flows', to='core.fanpage')),
],
),
migrations.CreateModel(
name='MessageLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sender_type', models.CharField(choices=[('user', 'User'), ('bot', 'Bot')], max_length=10)),
('message_text', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='core.chatsession')),
],
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Internal name for this step', max_length=255)),
('node_type', models.CharField(choices=[('text', 'Text Message'), ('buttons', 'Buttons/Quick Replies'), ('image', 'Image')], default='text', max_length=20)),
('content', models.JSONField(help_text='Stores the message text, button labels, etc.')),
('is_start_node', models.BooleanField(default=False)),
('flow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='nodes', to='core.flow')),
],
),
migrations.CreateModel(
name='Edge',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('condition', models.CharField(help_text='Keyword or button payload that triggers this edge', max_length=255)),
('flow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='edges', to='core.flow')),
('source_node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outgoing_edges', to='core.node')),
('target_node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incoming_edges', to='core.node')),
],
),
migrations.AddField(
model_name='chatsession',
name='current_node',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.node'),
),
migrations.AlterUniqueTogether(
name='chatsession',
unique_together={('psid', 'fanpage')},
),
]

View File

@ -1,3 +1,61 @@
from django.db import models from django.db import models
# Create your models here. class Fanpage(models.Model):
name = models.CharField(max_length=255)
page_id = models.CharField(max_length=255, unique=True)
access_token = models.TextField()
verify_token = models.CharField(max_length=255, help_text="Token for Facebook Webhook verification")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Flow(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
fanpage = models.ForeignKey(Fanpage, on_delete=models.CASCADE, related_name='flows')
is_default = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name} ({self.fanpage.name})"
class Node(models.Model):
NODE_TYPES = (
('text', 'Text Message'),
('buttons', 'Buttons/Quick Replies'),
('image', 'Image'),
)
flow = models.ForeignKey(Flow, on_delete=models.CASCADE, related_name='nodes')
name = models.CharField(max_length=255, help_text="Internal name for this step")
node_type = models.CharField(max_length=20, choices=NODE_TYPES, default='text')
content = models.JSONField(help_text="Stores the message text, button labels, etc.")
is_start_node = models.BooleanField(default=False)
def __str__(self):
return f"{self.name} [{self.node_type}]"
class Edge(models.Model):
flow = models.ForeignKey(Flow, on_delete=models.CASCADE, related_name='edges')
source_node = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='outgoing_edges')
target_node = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='incoming_edges')
condition = models.CharField(max_length=255, help_text="Keyword or button payload that triggers this edge")
def __str__(self):
return f"{self.source_node.name} -> {self.target_node.name} on '{self.condition}'"
class ChatSession(models.Model):
psid = models.CharField(max_length=255, help_text="Facebook Page Scoped ID of the user")
fanpage = models.ForeignKey(Fanpage, on_delete=models.CASCADE)
current_node = models.ForeignKey(Node, on_delete=models.SET_NULL, null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('psid', 'fanpage')
class MessageLog(models.Model):
session = models.ForeignKey(ChatSession, on_delete=models.CASCADE, related_name='logs')
sender_type = models.CharField(max_length=10, choices=(('user', 'User'), ('bot', 'Bot')))
message_text = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)

View File

@ -1,25 +1,53 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if project_description %} <title>{% block title %}FlowBot | Facebook Automation{% endblock %}</title>
<meta name="description" content="{{ project_description }}"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}"> <link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
{% block content %}{% endblock %} <nav class="navbar navbar-expand-lg navbar-dark sticky-top">
</body> <div class="container">
<a class="navbar-brand fw-bold" href="{% url 'home' %}">
<span class="text-coral">Flow</span>Bot
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'fanpage_list' %}">Fanpages</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'flow_list' %}">Flows</a></li>
<li class="nav-item"><a class="nav-link text-coral" href="/admin/">Admin</a></li>
<li class="nav-item">
<form action="{% url 'admin:logout' %}" method="post" class="d-inline">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link py-0">Logout</button>
</form>
</li>
{% else %}
<li class="nav-item"><a class="nav-link" href="/admin/login/">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% block content %}{% endblock %}
<footer class="py-4 mt-5 bg-indigo text-white-50">
<div class="container text-center">
<p class="mb-0">&copy; 2026 FlowBot Automation. Built with Django & Love.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html> </html>

View File

@ -0,0 +1,110 @@
{% extends 'base.html' %}
{% block title %}Dashboard | FlowBot{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row mb-4">
<div class="col">
<h2 class="fw-bold">Welcome back, {{ user.username }}!</h2>
<p class="text-muted">Here's what's happening with your bots today.</p>
</div>
<div class="col-auto">
<a href="/admin/core/fanpage/add/" class="btn btn-coral px-4">Connect Page</a>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-4">
<div class="card p-4 text-center">
<h6 class="text-uppercase text-muted small fw-bold">Active Pages</h6>
<div class="display-4 fw-bold text-indigo">{{ fanpage_count }}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 text-center">
<h6 class="text-uppercase text-muted small fw-bold">Total Flows</h6>
<div class="display-4 fw-bold text-indigo">{{ flow_count }}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 text-center">
<h6 class="text-uppercase text-muted small fw-bold">Today's Messages</h6>
<div class="display-4 fw-bold text-coral">{{ today_messages_count }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card p-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0 fw-bold">Connected Fanpages</h5>
<a href="{% url 'fanpage_list' %}" class="btn btn-sm btn-link text-indigo">View All</a>
</div>
<div class="table-responsive">
<table class="table align-middle">
<thead class="table-light">
<tr>
<th>Page Name</th>
<th>Status</th>
<th>Active Flow</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in fanpages %}
<tr>
<td>
<div class="fw-bold">{{ page.name }}</div>
<small class="text-muted">ID: {{ page.page_id }}</small>
</td>
<td>
{% if page.is_active %}
<span class="badge bg-success-subtle text-success">Active</span>
{% else %}
<span class="badge bg-danger-subtle text-danger">Paused</span>
{% endif %}
</td>
<td>
{% with page.flows.first as flow %}
{% if flow %}{{ flow.name }}{% else %}<span class="text-muted italic">None</span>{% endif %}
{% endwith %}
</td>
<td>
<a href="/admin/core/fanpage/{{ page.id }}/change/" class="btn btn-sm btn-outline-indigo">Manage</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">No pages connected yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Recent Activity</h5>
<div class="activity-feed">
{% for log in recent_logs %}
<div class="mb-3 border-bottom pb-3">
<div class="d-flex justify-content-between mb-1">
<span class="badge {% if log.sender_type == 'user' %}bg-info-subtle text-info{% else %}bg-indigo-subtle text-indigo{% endif %} px-2">
{{ log.sender_type|upper }}
</span>
<small class="text-muted">{{ log.timestamp|timesince }} ago</small>
</div>
<p class="small mb-0 text-truncate">{{ log.message_text }}</p>
</div>
{% empty %}
<p class="text-center text-muted py-5">No recent activity found.</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block title %}Fanpages | FlowBot{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Your Fanpages</h2>
<a href="/admin/core/fanpage/add/" class="btn btn-coral px-4">Add Fanpage</a>
</div>
<div class="row g-4">
{% for page in fanpages %}
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="d-flex justify-content-between mb-3">
<span class="badge {% if page.is_active %}bg-success{% else %}bg-secondary{% endif %}">
{% if page.is_active %}Active{% else %}Inactive{% endif %}
</span>
<small class="text-muted">Created {{ page.created_at|date:"M d, Y" }}</small>
</div>
<h4 class="fw-bold">{{ page.name }}</h4>
<p class="text-muted small">Page ID: {{ page.page_id }}</p>
<hr>
<div class="d-flex justify-content-between align-items-center mt-auto">
<span class="small">{{ page.flows.count }} Flow(s)</span>
<a href="/admin/core/fanpage/{{ page.id }}/change/" class="btn btn-sm btn-outline-indigo">Edit Settings</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="card p-5 text-center">
<h3 class="text-muted">No Fanpages found</h3>
<p>Connect your first Facebook Page to start automating!</p>
<a href="/admin/core/fanpage/add/" class="btn btn-coral mt-3 mx-auto" style="width: fit-content;">Connect Now</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% block title %}Flows | FlowBot{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Conversation Flows</h2>
<a href="/admin/core/flow/add/" class="btn btn-coral px-4">Create New Flow</a>
</div>
<div class="row g-4">
{% for flow in flows %}
<div class="col-md-6">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="fw-bold mb-0">{{ flow.name }}</h4>
{% if flow.is_default %}
<span class="badge bg-indigo">Default</span>
{% endif %}
</div>
<p class="text-muted mb-4">{{ flow.description|default:"No description provided." }}</p>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-light text-dark">{{ flow.fanpage.name }}</span>
<span class="small text-muted ms-2">{{ flow.nodes.count }} nodes</span>
</div>
<div class="btn-group">
<a href="/admin/core/flow/{{ flow.id }}/change/" class="btn btn-sm btn-outline-indigo">Edit</a>
<button class="btn btn-sm btn-indigo">Builder</button>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<div class="card p-5">
<h3 class="text-muted">No flows defined</h3>
<p>Start by creating a conversation flow for one of your pages.</p>
<a href="/admin/core/flow/add/" class="btn btn-coral mt-3 mx-auto" style="width: fit-content;">Create Flow</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -1,145 +1,75 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% load static %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<main> <section class="hero-section">
<div class="card"> <div class="container">
<h1>Analyzing your requirements and generating your app…</h1> <div class="row align-items-center">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <div class="col-lg-6">
<span class="sr-only">Loading…</span> <h1 class="display-3 fw-bold mb-4">Automate Your <span class="text-coral">Facebook</span> Conversations</h1>
<p class="lead mb-5 opacity-75">Build powerful, multi-page chatbots with a simple flow builder. Scale your customer service 24/7 without breaking a sweat.</p>
<div class="d-flex gap-3">
<a href="/admin/login/" class="btn btn-coral btn-lg px-4 py-3">Get Started Free</a>
<a href="#features" class="btn btn-outline-light btn-lg px-4 py-3">How it Works</a>
</div> </div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div> </div>
</main> <div class="col-lg-5 offset-lg-1 d-none d-lg-block">
<footer> <div class="glass-card">
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) <div class="d-flex mb-4 align-items-center">
</footer> <div class="bg-coral rounded-circle p-2 me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-indigo"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
</div>
<h5 class="mb-0">Smart Auto-Reply</h5>
</div>
<div class="bg-white rounded p-3 mb-3 text-dark shadow-sm">
<small class="text-muted d-block mb-1">Customer</small>
"What are your opening hours?"
</div>
<div class="bg-indigo rounded p-3 text-white shadow-sm ms-5">
<small class="text-coral d-block mb-1">FlowBot</small>
"Hi there! We are open from 9 AM to 6 PM daily. Would you like to see our menu?"
</div>
</div>
</div>
</div>
</div>
</section>
<section id="features" class="py-5">
<div class="container py-5">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold">Why Choose FlowBot?</h2>
<p class="text-muted">The ultimate tool for multi-fanpage management.</p>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="text-coral mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
</div>
<h4>Multi-Page Support</h4>
<p class="text-muted">Connect and manage unlimited Fanpages from a single dashboard. Each page can have its own unique flow.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="text-coral mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
</div>
<h4>Flow Builder</h4>
<p class="text-muted">Design complex conversation trees with buttons, quick replies, and images. No coding required.</p>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="text-coral mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
</div>
<h4>Real-time Analytics</h4>
<p class="text-muted">Track bot performance and view chat history in real-time to optimize your customer experience.</p>
</div>
</div>
</div>
</div>
</section>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,11 @@
from django.urls import path from django.urls import path
from . import views
from .views import home
urlpatterns = [ urlpatterns = [
path("", home, name="home"), path('', views.home, name='home'),
path('dashboard/', views.dashboard, name='dashboard'),
path('dashboard/webhook/', views.webhook, name='webhook_dashboard'),
path('webhook/', views.webhook, name='webhook'),
path('fanpages/', views.fanpage_list, name='fanpage_list'),
path('flows/', views.flow_list, name='flow_list'),
] ]

173
core/utils.py Normal file
View File

@ -0,0 +1,173 @@
import requests
import logging
from .models import Fanpage, Flow, Node, Edge, ChatSession, MessageLog
from ai.local_ai_api import LocalAIApi
logger = logging.getLogger(__name__)
def send_fb_message(psid, access_token, message_content):
"""
Sends a message to a Facebook user via the Graph API.
message_content: dict matching Facebook's message object (e.g., {"text": "..."})
"""
url = f"https://graph.facebook.com/v12.0/me/messages?access_token={access_token}"
payload = {
"recipient": {"id": psid},
"message": message_content
}
try:
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Error sending message to Facebook: {e}")
# Return a dummy success if we are just testing with a placeholder token
if access_token == "YOUR_FACEBOOK_PAGE_ACCESS_TOKEN" or not access_token:
return {"message_id": "mid.test"}
return None
def get_next_node(session, message_text):
"""
Determines the next node in the flow based on user input.
Returns None if no matching edge is found.
"""
fanpage = session.fanpage
if not session.current_node:
# Start of conversation: find the default flow and its start node
flow = Flow.objects.filter(fanpage=fanpage, is_default=True).first()
if not flow:
flow = Flow.objects.filter(fanpage=fanpage).first()
if flow:
return Node.objects.filter(flow=flow, is_start_node=True).first()
return None
# We are in an active session, look for a matching edge
edges = Edge.objects.filter(source_node=session.current_node)
message_text_clean = message_text.strip().lower()
for edge in edges:
if edge.condition.lower() == message_text_clean:
return edge.target_node
# If no matching edge, return None to trigger AI fallback
return None
def get_ai_fallback_response(message_text, session):
"""
Generates an AI-powered response when no flow edge matches.
"""
fanpage = session.fanpage
# Get last 5 messages for context
recent_logs = MessageLog.objects.filter(session=session).order_by('-timestamp')[:5]
history = []
# Reverse to get chronological order
for log in reversed(list(recent_logs)):
role = "user" if log.sender_type == 'user' else "assistant"
history.append({"role": role, "content": log.message_text})
system_prompt = f"You are a helpful AI assistant for the Facebook Page '{fanpage.name}'. "
system_prompt += "Your goal is to answer questions and help users in a friendly manner. "
if session.current_node:
system_prompt += f"The user is currently at the stage '{session.current_node.name}' in our automated flow, but just asked something else."
messages = [{"role": "system", "content": system_prompt}] + history
logger.info(f"Triggering AI fallback for session {session.id}")
response = LocalAIApi.create_response({
"input": messages
})
if response.get("success"):
ai_text = LocalAIApi.extract_text(response)
if ai_text:
return ai_text
return "I'm sorry, I couldn't understand that. How else can I help you today?"
def handle_webhook_event(data):
"""
Main entry point for processing Facebook webhook POST data.
"""
if data.get('object') != 'page':
logger.info(f"Webhook event received but 'object' is not 'page': {data.get('object')}")
return
for entry in data.get('entry', []):
for messaging_event in entry.get('messaging', []):
sender_id = messaging_event.get('sender', {}).get('id')
recipient_id = messaging_event.get('recipient', {}).get('id')
logger.info(f"Processing messaging event: sender={sender_id}, recipient={recipient_id}")
if not sender_id or not recipient_id:
continue
# 1. Identify Fanpage
try:
fanpage = Fanpage.objects.get(page_id=recipient_id, is_active=True)
except Fanpage.DoesNotExist:
logger.warning(f"Received event for unknown or inactive Page ID: {recipient_id}")
continue
# 2. Extract Message
message_text = ""
if 'message' in messaging_event:
message_text = messaging_event['message'].get('text', "")
elif 'postback' in messaging_event:
# Handle button clicks
message_text = messaging_event['postback'].get('payload', "")
if not message_text:
logger.info("No message text or postback payload found in event.")
continue
# 3. Get or Create Session
session, created = ChatSession.objects.get_or_create(
psid=sender_id,
fanpage=fanpage
)
# 4. Log User Message
MessageLog.objects.create(
session=session,
sender_type='user',
message_text=message_text
)
logger.info(f"Logged user message: {message_text[:20]}...")
# 5. Determine Next Node
next_node = get_next_node(session, message_text)
if next_node:
# 6. Send Flow Reply
bot_text = next_node.content.get('text', '[Non-text message]')
MessageLog.objects.create(
session=session,
sender_type='bot',
message_text=bot_text
)
# Update Session
session.current_node = next_node
session.save()
# Actual delivery attempt
send_fb_message(sender_id, fanpage.access_token, next_node.content)
logger.info(f"Sent flow reply: {bot_text[:20]}...")
else:
# 9. Trigger AI Fallback
ai_reply_text = get_ai_fallback_response(message_text, session)
# Log AI Response first so it shows up even if delivery fails
MessageLog.objects.create(
session=session,
sender_type='bot',
message_text=ai_reply_text
)
# Actual delivery attempt
send_fb_message(sender_id, fanpage.access_token, {"text": ai_reply_text})
logger.info(f"Sent AI fallback reply: {ai_reply_text[:20]}...")

View File

@ -1,25 +1,85 @@
import os from django.shortcuts import render, redirect, get_object_or_404
import platform from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django import get_version as django_version from django.http import HttpResponse
from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
import json
import logging
from .models import Fanpage, Flow, MessageLog, ChatSession
from .utils import handle_webhook_event
logger = logging.getLogger(__name__)
def home(request): def home(request):
"""Render the landing screen with loader and environment details.""" if request.user.is_authenticated:
host_name = request.get_host().lower() return redirect('dashboard')
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" return render(request, 'core/index.html')
now = timezone.now()
@login_required
def dashboard(request):
fanpages = Fanpage.objects.all()
flows = Flow.objects.all()
recent_logs = MessageLog.objects.order_by('-timestamp')[:10]
# Count messages from today
today = timezone.now().date()
today_messages_count = MessageLog.objects.filter(timestamp__date=today).count()
context = { context = {
"project_name": "New Style", 'fanpage_count': fanpages.count(),
"agent_brand": agent_brand, 'flow_count': flows.count(),
"django_version": django_version(), 'fanpages': fanpages,
"python_version": platform.python_version(), 'recent_logs': recent_logs,
"current_time": now, 'today_messages_count': today_messages_count,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
} }
return render(request, "core/index.html", context) return render(request, 'core/dashboard.html', context)
@login_required
def fanpage_list(request):
fanpages = Fanpage.objects.all()
return render(request, 'core/fanpage_list.html', {'fanpages': fanpages})
@login_required
def flow_list(request):
flows = Flow.objects.all()
return render(request, 'core/flow_list.html', {'flows': flows})
@csrf_exempt
def webhook(request):
if request.method == 'GET':
# Facebook Webhook verification
mode = request.GET.get('hub.mode')
token = request.GET.get('hub.verify_token')
challenge = request.GET.get('hub.challenge')
if mode == 'subscribe' and token:
print(f"DEBUG: Webhook verification attempt - mode: {mode}, token: {token}")
# Check if this token matches any Fanpage verify_token
# or the default 'pages_messaging'
if Fanpage.objects.filter(verify_token=token).exists() or token == 'pages_messaging':
print(f"DEBUG: Webhook verification SUCCESS")
return HttpResponse(challenge)
else:
print(f"DEBUG: Webhook verification FAILED - Token mismatch: {token}")
return HttpResponse('Verification failed: Token mismatch', status=403)
# If no parameters, return 200 with instructions instead of 403 to avoid confusion
if not mode and not token:
print(f"DEBUG: Webhook endpoint accessed via browser (GET without parameters)")
return HttpResponse('Facebook Webhook Endpoint is active. Please use this URL in your Facebook App settings for Webhooks configuration.', status=200)
return HttpResponse('Verification failed: Missing parameters', status=403)
elif request.method == 'POST':
# Handle incoming messages
try:
data = json.loads(request.body.decode('utf-8'))
# Process the webhook payload
handle_webhook_event(data)
return HttpResponse('EVENT_RECEIVED')
except Exception as e:
# Log the error if necessary
print(f"ERROR processing webhook: {str(e)}")
return HttpResponse('Error processing request', status=400)
return HttpResponse('Method not allowed', status=405)

View File

@ -1,3 +1,4 @@
Django==5.2.7 Django==5.2.7
mysqlclient==2.2.7 mysqlclient==2.2.7
python-dotenv==1.1.1 python-dotenv==1.1.1
requests==2.31.0

View File

@ -1,4 +1,74 @@
/* Custom styles for the application */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;600;700&display=swap');
body {
font-family: system-ui, -apple-system, sans-serif; :root {
--fb-indigo: #2D3250;
--fb-slate: #424769;
--fb-grey: #7077A1;
--fb-coral: #F6B17A;
--fb-offwhite: #F0F0F0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--fb-offwhite);
color: var(--fb-indigo);
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
}
.bg-indigo { background-color: var(--fb-indigo) !important; }
.bg-slate { background-color: var(--fb-slate) !important; }
.text-coral { color: var(--fb-coral) !important; }
.btn-coral {
background-color: var(--fb-coral);
color: var(--fb-indigo);
font-weight: 600;
border: none;
transition: all 0.3s ease;
}
.btn-coral:hover {
background-color: #e5a069;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(246, 177, 122, 0.3);
}
.navbar {
background-color: rgba(45, 50, 80, 0.95);
backdrop-filter: blur(10px);
}
.card {
border: none;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.05);
}
.hero-section {
background: linear-gradient(135deg, var(--fb-indigo) 0%, var(--fb-slate) 100%);
color: white;
padding: 100px 0;
position: relative;
overflow: hidden;
}
.hero-section::after {
content: '';
position: absolute;
top: -10%;
right: -5%;
width: 400px;
height: 400px;
background: radial-gradient(circle, var(--fb-coral) 0%, transparent 70%);
opacity: 0.1;
border-radius: 50%;
}
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 30px;
} }

View File

@ -1,21 +1,74 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;600;700&display=swap');
:root { :root {
--bg-color-start: #6a11cb; --fb-indigo: #2D3250;
--bg-color-end: #2575fc; --fb-slate: #424769;
--text-color: #ffffff; --fb-grey: #7077A1;
--card-bg-color: rgba(255, 255, 255, 0.01); --fb-coral: #F6B17A;
--card-border-color: rgba(255, 255, 255, 0.1); --fb-offwhite: #F0F0F0;
} }
body { body {
margin: 0;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); background-color: var(--fb-offwhite);
color: var(--text-color); color: var(--fb-indigo);
display: flex; }
justify-content: center;
align-items: center; h1, h2, h3, h4, h5, h6 {
min-height: 100vh; font-family: 'Outfit', sans-serif;
text-align: center; }
overflow: hidden;
position: relative; .bg-indigo { background-color: var(--fb-indigo) !important; }
.bg-slate { background-color: var(--fb-slate) !important; }
.text-coral { color: var(--fb-coral) !important; }
.btn-coral {
background-color: var(--fb-coral);
color: var(--fb-indigo);
font-weight: 600;
border: none;
transition: all 0.3s ease;
}
.btn-coral:hover {
background-color: #e5a069;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(246, 177, 122, 0.3);
}
.navbar {
background-color: rgba(45, 50, 80, 0.95);
backdrop-filter: blur(10px);
}
.card {
border: none;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.05);
}
.hero-section {
background: linear-gradient(135deg, var(--fb-indigo) 0%, var(--fb-slate) 100%);
color: white;
padding: 100px 0;
position: relative;
overflow: hidden;
}
.hero-section::after {
content: '';
position: absolute;
top: -10%;
right: -5%;
width: 400px;
height: 400px;
background: radial-gradient(circle, var(--fb-coral) 0%, transparent 70%);
opacity: 0.1;
border-radius: 50%;
}
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 30px;
} }