Production latest code push

This commit is contained in:
root 2025-03-02 01:52:37 +00:00
commit 2fc5bb1b21
348 changed files with 37301 additions and 0 deletions

30
.env Normal file
View File

@ -0,0 +1,30 @@
# Database Configuration
POSTGRES_DB=polisplexity
POSTGRES_USER=postgres
POSTGRES_PASSWORD=mysecretpassword
# Django Environment Variables
DEBUG=True
SECRET_KEY=django-insecure-%*=%u3gv38cv*2iwy)m^)flo3p4w7ol*n5*-7lr*i4^u+(v=#q
ALLOWED_HOSTS=127.0.0.1,localhost,app.polisplexity.tech,191.101.233.39
# Database URL
DATABASE_URL=postgres://postgres:mysecretpassword@db:5432/polisplexity
# Static and Media Files
STATIC_URL=/static/
STATIC_ROOT=/app/static
MEDIA_URL=/media/
MEDIA_ROOT=/app/media
# Neo4j Database Configuration
NEO4J_URI=neo4j+s://74d433fb.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=4Y5-ppefHkgEiLr-l0qzbf8wNJw0zkOmRmk7cSkSrTg
# OpenAI API Key
OPENAI_API_KEY=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
# Facebook API Tokens
PAGE_ACCESS_TOKEN=EAAIq9z4rVPIBOxJxRnmbjIUsqJ9ZB5hZC9MF4qN64VNpxUCYguMCqUNKSsAjQZAcD9hlhZCv2RcV4GOIFC3Ni6VGoMp3rTFlLwtXxFIklj0FqZAVqSh7i0QT3Kwt9SCx9V9iioSsyFhUQrnpTXZCoDPJy0i2kMkzkY5ZA58hieSeQZBZARz3ZC7XeZCi5uSZBXYCeatGuAZDZD
VERIFY_TOKEN=YzQ2VWcODWO922j30HZ9AV113kAisTjcacc3wzURPvFjHCOWjcYP39ThgCWlPQ1w

0
.env:Zone.Identifier Normal file
View File

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
# Use official Python image
FROM python:3.10
# Set working directory
WORKDIR /app
# Copy application code
COPY . .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Expose the application port
EXPOSE 8000
# Run Gunicorn server
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "polisplexity.wsgi:application"]

0
core/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
core/admin.py Normal file
View File

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import Category, UserMenu
admin.site.register(Category)
admin.site.register(UserMenu)

6
core/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"

View File

@ -0,0 +1,29 @@
# Generated by Django 5.0.3 on 2024-03-16 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("description", models.TextField(blank=True, null=True)),
],
),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 5.0.3 on 2024-03-16 20:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="UserMenu",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("order", models.IntegerField(default=0)),
("description", models.TextField()),
("icon", models.CharField(blank=True, max_length=50, null=True)),
(
"image",
models.ImageField(
blank=True, null=True, upload_to="media/images/menu/"
),
),
("url", models.CharField(blank=True, max_length=200, null=True)),
("url_text", models.CharField(blank=True, max_length=50, null=True)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_menus",
to="core.category",
),
),
],
options={
"ordering": ["category", "order"],
},
),
]

View File

Binary file not shown.

28
core/models.py Normal file
View File

@ -0,0 +1,28 @@
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True)
def __str__(self):
return self.name
class UserMenu(models.Model):
title = models.CharField(max_length=100)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='user_menus') # Assuming each menu belongs to one category
order = models.IntegerField(default=0)
description = models.TextField()
icon = models.CharField(max_length=50, blank=True, null=True)
image = models.ImageField(upload_to='media/images/menu/', null=True, blank=True) # Changed 'media/images/' to 'user_menu_images/' for clarity
url = models.CharField(max_length=200, blank=True, null=True)
url_text = models.CharField(max_length=50, blank=True, null=True)
def __str__(self):
return self.title
class Meta:
ordering = ['category', 'order'] # Orders by category first, then by the order field within each category

View File

@ -0,0 +1,66 @@
{% extends 'base.html' %}
{% block title %}Polisplexity Portal{% endblock title %}
{% block extra_css %}
<style>
.card-img-top {
height: 200px; /* Fixed height for consistency */
object-fit: cover; /* Ensures image covers the area nicely */
}
.card:hover {
transform: scale(1.05); /* Slight zoom effect on hover for interactivity */
transition: transform 0.3s ease-in-out;
}
</style>
{% endblock extra_css %}
{% block content %}
<div class="container mt-4">
<h2 class="text-center mb-4">Welcome to Polisplexity</h2>
<p class="text-center mb-4">Select one of the options Bellow</p>
{% for category, items in grouped_menu_items.items %}
{% if not forloop.first %}
<hr> <!-- Horizontal separator for every new category except the first one -->
{% endif %}
<div class="row">
<div class="col-12 col-md-3 mb-4">
<h3> {{ category.name }}</h3>
<small> {{ category.description }}</small>
</div>
{% for menu_item in items %}
<div class="col-12 col-md-3 mb-4">
<div class="card h-100">
{% if menu_item.image %}
<!-- Display Image if available -->
<img src="{{ menu_item.image.url }}" class="card-img-top" alt="{{ menu_item.title }}">
{% elif menu_item.icon %}
<!-- Display Icon if image is not available but icon is -->
<div class="card-header text-center">
<span class="material-symbols-rounded md-48">{{ menu_item.icon }}</span>
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ menu_item.title }}</h5>
<p class="card-text">{{ menu_item.description }}</p>
</div>
<div class="card-footer bg-white">
<a href="{{ menu_item.url }}" class="btn btn-primary">{{ menu_item.url_text }}</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endblock content %}
{% block extra_js %}
<script>
$(function () {
$('[data-bs-toggle="tooltip"]').tooltip() // Initialize Bootstrap tooltips
})
</script>
{% endblock extra_js %}

3
core/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

6
core/urls.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import path
from .views import index
urlpatterns = [
path('', index, name='index'),
]

11
core/views.py Normal file
View File

@ -0,0 +1,11 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import UserMenu
from itertools import groupby
def index(request):
menu_items = UserMenu.objects.all().order_by('category', 'order')
# Group by category
grouped_menu_items = {k: list(v) for k, v in groupby(menu_items, lambda x: x.category)}
return render(request, 'core/index.html', {'grouped_menu_items': grouped_menu_items})

BIN
db.sqlite3 Normal file

Binary file not shown.

39
docker-compose.yml Normal file
View File

@ -0,0 +1,39 @@
version: "3.8"
services:
db:
image: postgres:15
container_name: polisplexity_postgres
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
env_file:
- .env
ports:
- "5434:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
retries: 5
timeout: 5s
web:
build: .
container_name: polisplexity_django
restart: always
depends_on:
db:
condition: service_healthy
volumes:
- .:/app # Ensure correct project structure
ports:
- "8010:8001"
env_file:
- .env
command: >
sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput &&
exec gunicorn --bind 0.0.0.0:8001 polisplexity.wsgi:application"
volumes:
pgdata:

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polisplexity.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

11
pgloader.load Normal file
View File

@ -0,0 +1,11 @@
LOAD DATABASE
FROM sqlite:///app/db.sqlite3
INTO postgresql://postgres:mysecretpassword@localhost:5434/polisplexity
WITH include no drop, create tables, create indexes, reset sequences
SET work_mem to '16MB', maintenance_work_mem to '512 MB'
ALTER SCHEMA 'main' RENAME TO 'public'
;

0
polisplexity/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
polisplexity/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for polisplexity project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polisplexity.settings")
application = get_asgi_application()

127
polisplexity/settings.py Normal file
View File

@ -0,0 +1,127 @@
"""
Django settings for polisplexity project.
Generated by 'django-admin startproject' using Django 5.0.3.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
import os
import dj_database_url
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
BASE_URL = "https://app.polisplexity.tech"
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DEBUG") == "True"
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"core",
"pxy_de",
"pxy_cr",
"pxy_whatsapp",
"pxy_city_digital_twins",
"pxy_bots",
"pxy_openai",
"pxy_meta_pages",
"pxy_langchain",
"pxy_neo4j",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "polisplexity.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "polisplexity.wsgi.application"
# Database Configuration
DATABASES = {
"default": dj_database_url.config(default=os.getenv("DATABASE_URL"))
}
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static") # Ensure this line is correct
# Add this if missing
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # Fixes mixed content issues
CSRF_TRUSTED_ORIGINS = ['https://app.polisplexity.tech'] # Allow CSRF over HTTPS
MEDIA_URL = "/media/"
MEDIA_ROOT = os.getenv("MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
# Default primary key field type
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Facebook API Tokens
PAGE_ACCESS_TOKEN = os.getenv("PAGE_ACCESS_TOKEN")
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
# Neo4j Database Configuration
NEO4J_URI = os.getenv("NEO4J_URI")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
# OpenAI API Key
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

36
polisplexity/urls.py Normal file
View File

@ -0,0 +1,36 @@
"""
URL configuration for polisplexity project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
admin.site.site_header = "Polisplexity City Technologies"
admin.site.site_title = "Polisplexity Admin Portal"
admin.site.index_title = "Welcome to Polisplexity City Technologies Portal"
urlpatterns = [
path("admin/", admin.site.urls),
path('', include('core.urls')),
path('', include('pxy_city_digital_twins.urls')),
path('pxy_whatsapp/', include('pxy_whatsapp.urls')),
path('bots/', include('pxy_bots.urls')), # Webhook URL: /bots/webhook/<bot_name>/
path('pxy_meta_pages/', include('pxy_meta_pages.urls', namespace='pxy_meta_pages')),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
polisplexity/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for polisplexity project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polisplexity.settings")
application = get_wsgi_application()

2149
polisplexity_dump.sql Normal file

File diff suppressed because it is too large Load Diff

0
pxy_bots/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

52
pxy_bots/admin.py Normal file
View File

@ -0,0 +1,52 @@
from django.contrib import admin
from .models import TelegramBot
from django.utils.html import format_html
@admin.register(TelegramBot)
class TelegramBotAdmin(admin.ModelAdmin):
list_display = ("name", "username", "is_active", "get_assistant_name", "set_webhook_action")
search_fields = ("name", "username")
list_filter = ("is_active",)
actions = ["set_webhooks"]
@admin.action(description="Set webhooks for selected bots")
def set_webhooks(self, request, queryset):
base_url = request.build_absolute_uri("/")[:-1] # Get base server URL
results = []
for bot in queryset:
if bot.is_active:
try:
result = bot.set_webhook(base_url)
self.message_user(
request,
f"Webhook set for {bot.name}: {result}",
level="success",
)
except Exception as e:
self.message_user(
request,
f"Failed to set webhook for {bot.name}: {str(e)}",
level="error",
)
results.append(result)
else:
self.message_user(
request,
f"Skipped inactive bot: {bot.name}",
level="warning",
)
def get_assistant_name(self, obj):
"""Show the name of the assistant linked to the bot."""
return obj.assistant.name if obj.assistant else "None"
get_assistant_name.short_description = "Assistant Name"
def set_webhook_action(self, obj):
"""Button in the Django admin to manually trigger webhook setup."""
return format_html(
'<a class="button" href="{}">Set Webhook</a>',
f"/admin/pxy_bots/set_webhook/{obj.id}/"
)
set_webhook_action.short_description = "Webhook"

6
pxy_bots/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PxyBotsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pxy_bots"

73
pxy_bots/handlers.py Normal file
View File

@ -0,0 +1,73 @@
from telegram import Update, ForceReply
import logging
from pxy_openai.assistants import OpenAIAssistant
from .models import TelegramBot
from asgiref.sync import sync_to_async
from pxy_langchain.models import AIAssistant
from pxy_langchain.services import LangchainAIService
logger = logging.getLogger(__name__)
async def dream_city_command(update: Update):
"""Send a message with a link to the random city generator."""
message = (
"Descubre la ciudad de tus sueños! Haz clic en el enlace para explorar una ciudad generada aleatoriamente que podría reflejar tus aspiraciones urbanas: "
"https://app.polisplexity.tech/city/digital/twin/dream/?innovation=30&technology=30&science=40"
)
await update.message.reply_text(message)
async def start(update: Update):
"""Send a message when the command /start is issued."""
user = update.effective_user
await update.message.reply_html(
rf"Hi {user.mention_html()}!",
reply_markup=ForceReply(selective=True),
)
async def help_command(update: Update):
"""Send a message when the command /help is issued."""
user = update.effective_user
await update.message.reply_text(f"Help! How can I assist you, {user.first_name}?")
async def handle_location(update: Update):
"""Respond to a location message."""
location = update.message.location
if location:
await update.message.reply_text(
f"Thanks for sharing your location! Latitude: {location.latitude}, Longitude: {location.longitude}"
)
else:
await update.message.reply_text("Please share your location.")
async def respond(update, bot_name):
"""Respond to user messages using the LangChain AI service."""
try:
user_message = update.message.text
# Fetch the Telegram bot and its assigned AI assistant asynchronously
telegram_bot = await sync_to_async(TelegramBot.objects.get)(name=bot_name, is_active=True)
if not telegram_bot.assistant:
raise ValueError(f"No assistant configured for bot '{bot_name}'.")
# Fetch the AI assistant linked to this bot
assistant = await sync_to_async(AIAssistant.objects.get)(name=telegram_bot.assistant.name)
# Initialize AI service
ai_service = LangchainAIService(assistant)
# Generate response asynchronously
bot_response = await sync_to_async(ai_service.generate_response)(user_message)
await update.message.reply_text(bot_response)
except TelegramBot.DoesNotExist:
await update.message.reply_text(f"Bot '{bot_name}' not found or inactive.")
except Exception as e:
await update.message.reply_text(f"Oops! Something went wrong: {e}")

View File

@ -0,0 +1,24 @@
# Generated by Django 4.0.6 on 2025-01-16 12:39
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='TelegramBot',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text="Name of the bot (e.g., 'SupportBot').", max_length=50, unique=True)),
('username', models.CharField(help_text="The bot's Telegram username (e.g., 'SupportBot').", max_length=50, unique=True)),
('token', models.CharField(help_text='Telegram Bot API token.', max_length=200, unique=True)),
('is_active', models.BooleanField(default=True, help_text='Is this bot active?')),
],
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.0.3 on 2025-01-16 19:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pxy_bots", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="telegrambot",
name="is_active",
field=models.BooleanField(
default=True, help_text="Indicates if this bot is active."
),
),
migrations.AlterField(
model_name="telegrambot",
name="name",
field=models.CharField(
help_text="Bot name (e.g., 'SupportBot').", max_length=50, unique=True
),
),
migrations.AlterField(
model_name="telegrambot",
name="token",
field=models.CharField(
help_text="Telegram bot token.", max_length=200, unique=True
),
),
migrations.AlterField(
model_name="telegrambot",
name="username",
field=models.CharField(
help_text="Bot username (e.g., 'SupportBot').",
max_length=50,
unique=True,
),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 5.0.3 on 2025-01-28 21:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"pxy_bots",
"0002_alter_telegrambot_is_active_alter_telegrambot_name_and_more",
),
("pxy_openai", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="telegrambot",
name="assistant",
field=models.ForeignKey(
default=1,
help_text="The OpenAI assistant associated with this Telegram bot.",
on_delete=django.db.models.deletion.CASCADE,
related_name="telegram_bots",
to="pxy_openai.openaiassistant",
),
preserve_default=False,
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 5.0.3 on 2025-01-29 02:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("pxy_bots", "0003_telegrambot_assistant"),
]
operations = [
migrations.RemoveField(
model_name="telegrambot",
name="assistant",
),
migrations.AddField(
model_name="telegrambot",
name="assistant_id",
field=models.PositiveIntegerField(
default=1, help_text="The ID of the associated assistant."
),
preserve_default=False,
),
migrations.AddField(
model_name="telegrambot",
name="assistant_type",
field=models.ForeignKey(
default=1,
help_text="The assistant model type.",
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
preserve_default=False,
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 5.0.3 on 2025-01-29 03:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pxy_bots", "0004_remove_telegrambot_assistant_and_more"),
(
"pxy_langchain",
"0002_remove_aiprovider_base_url_aiassistant_created_at_and_more",
),
]
operations = [
migrations.RemoveField(
model_name="telegrambot",
name="assistant_id",
),
migrations.RemoveField(
model_name="telegrambot",
name="assistant_type",
),
migrations.AddField(
model_name="telegrambot",
name="assistant",
field=models.ForeignKey(
default=1,
help_text="The LangChain AI assistant associated with this Telegram bot.",
on_delete=django.db.models.deletion.CASCADE,
related_name="telegram_bots",
to="pxy_langchain.aiassistant",
),
preserve_default=False,
),
]

View File

47
pxy_bots/models.py Normal file
View File

@ -0,0 +1,47 @@
from django.db import models
from pxy_langchain.models import AIAssistant # Now referencing LangChain AI assistants
class TelegramBot(models.Model):
"""
Represents a Telegram bot that interacts with users using a LangChain AI assistant.
"""
name = models.CharField(max_length=50, unique=True, help_text="Bot name (e.g., 'SupportBot').")
username = models.CharField(max_length=50, unique=True, help_text="Bot username (e.g., 'SupportBot').")
token = models.CharField(max_length=200, unique=True, help_text="Telegram bot token.")
is_active = models.BooleanField(default=True, help_text="Indicates if this bot is active.")
assistant = models.ForeignKey(
AIAssistant,
on_delete=models.CASCADE,
related_name="telegram_bots",
help_text="The LangChain AI assistant associated with this Telegram bot.",
)
def __str__(self):
return f"{self.name} (@{self.username})"
@staticmethod
def get_bot_token(bot_name):
"""Retrieve the token for the given bot name."""
try:
bot = TelegramBot.objects.get(name=bot_name, is_active=True)
return bot.token
except TelegramBot.DoesNotExist:
raise ValueError(f"Bot with name '{bot_name}' not found or inactive.")
def set_webhook(self, base_url):
"""
Set the webhook for this bot dynamically based on the server's base URL.
"""
if not self.is_active:
raise ValueError(f"Bot '{self.name}' is inactive. Activate it before setting the webhook.")
webhook_url = f"{base_url}/bots/webhook/{self.name}/"
response = requests.post(
f"https://api.telegram.org/bot{self.token}/setWebhook",
data={"url": webhook_url}
)
if response.status_code == 200:
return response.json()
else:
raise ValueError(f"Failed to set webhook for {self.name}: {response.json()}")

16
pxy_bots/set_webhook.py Normal file
View File

@ -0,0 +1,16 @@
import requests
from pxy_bots.models import TelegramBot
BASE_URL = "https://your-domain.com/bots/webhook/"
def set_telegram_webhooks():
"""Sets webhooks for all active bots."""
bots = TelegramBot.objects.filter(is_active=True)
for bot in bots:
webhook_url = f"{BASE_URL}{bot.name}/"
response = requests.post(
f"https://api.telegram.org/bot{bot.token}/setWebhook",
data={"url": webhook_url}
)
print(f"Webhook for {bot.name} ({bot.username}) set to: {webhook_url}")
print("Response:", response.json())

209
pxy_bots/telgram_bot.py Normal file
View File

@ -0,0 +1,209 @@
import logging
from openai import OpenAI
from telegram import ForceReply, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
import json
import random
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger(__name__)
async def dream_city_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send a message with a link to the random city generator."""
# Construct the message
message = (
"Descubre la ciudad de tus sueños! Haz clic en el enlace para explorar una ciudad generada aleatoriamente que podría reflejar tus aspiraciones urbanas: "
"https://app.polisplexity.tech/city/digital/twin/dream/?innovation=30&technology=30&science=40"
)
# Send the message
await update.message.reply_text(message)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send a message when the command /start is issued."""
user = update.effective_user
await update.message.reply_html(
rf"Hi {user.mention_html()}!",
reply_markup=ForceReply(selective=True),
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send a message when the command /help is issued."""
user = update.effective_user
location = update.message.location
await update.message.reply_text(rf"Help! {user.mention_html()} in {location}")
async def handle_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Respond to a location message."""
location = update.message.location
if location:
# Extract latitude and longitude
latitude = location.latitude
longitude = location.longitude
# You can now use the latitude and longitude for your bot's purposes
await update.message.reply_text(f"Thanks for sharing your location! Latitude: {latitude}, Longitude: {longitude}")
else:
# Respond if no location is found in the message
await update.message.reply_text("Please share your location.")
async def assistant(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Respond to user message using OpenAI."""
bot_response = "Thinking..."
try:
user_message = update.message.text
client = OpenAI(api_key='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',)
neurolitiks_assistant = client.beta.assistants.retrieve("asst_LcyxtrwgxgdHoVwSsuTUf5Ec")
my_thread = client.beta.threads.create()
my_thread_message = client.beta.threads.messages.create(
thread_id=my_thread.id,
role="user",
content=user_message,
)
my_run = client.beta.threads.runs.create(
thread_id=my_thread.id,
assistant_id=neurolitiks_assistant.id,
)
# Step 6: Periodically retrieve the Run to check on its status to see if it has moved to completed
while my_run.status in ["queued", "in_progress"]:
keep_retrieving_run = client.beta.threads.runs.retrieve(
thread_id=my_thread.id,
run_id=my_run.id
)
print(keep_retrieving_run)
if keep_retrieving_run.status == "completed":
# Step 7: Retrieve the Messages added by the Assistant to the Thread
all_messages = client.beta.threads.messages.list(
thread_id=my_thread.id
)
bot_response = all_messages.data[0].content[0].text.value #openai_response.choices[0].message.content
break
elif keep_retrieving_run.status == "queued" or keep_retrieving_run.status == "in_progress":
# bot_response += ' ' + my_run.status
pass
else:
bot_response += ' ' + keep_retrieving_run.status
break
if bot_response:
await update.message.reply_text(bot_response)
else:
# In case the response is empty or only whitespace
await update.message.reply_text("I'm not sure how to respond to that. 🤔")
except Exception as e:
logger.error(f"Error while processing the message: {e}")
# Send a default response in case of an error
await update.message.reply_text(f"Oops, I encountered an issue. Please try again later. 😓 {e}")
def prepare_persona_feedback(user_query):
# Example function to fetch and format feedback based on user query
# This could query a database or API for more dynamic data
persona_feedback = {
"Other Citizens": "Will analyze and propose community driven improvements.",
"Technology Makers": "Will analyze integration with technologies, and will say what technologies and how.",
"Scientific Innovators": "Will analyze from the urbanist persepective first, and then will recommen what scientific or urban models to use"
}
return persona_feedback
def generate_system_prompt(persona_feedback):
return (f"Based on the urban improvement proposal, seek feedback from various perspectives: "
f"Other Citizens suggest: {persona_feedback['Other Citizens']}; "
f"Technology Makers recommend: {persona_feedback['Technology Makers']}; "
f"Scientific Innovators advise: {persona_feedback['Scientific Innovators']}. "
"Evaluate and compile these suggestions to enhance the proposal.")
async def respond(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Respond to user message using OpenAI."""
bot_response = "Thinking..."
try:
user_query = update.message.text
client = OpenAI(api_key='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')
# Assuming prepare_persona_feedback and generate_system_prompt are defined elsewhere
persona_feedback = prepare_persona_feedback(user_query)
system_prompt = generate_system_prompt(persona_feedback)
openai_response = client.chat.completions.create(
model="gpt-3.5-turbo-0125",
messages=[
{"role": "system", "content": "Act as a citizen technology advisor. You will receive enquiries from citizens and will advise on what technologies they could use to resolve them."},
{"role": "user", "content": user_query}
],
)
bot_response = openai_response.choices[0].message.content
# Send the main response
if bot_response:
await update.message.reply_text(bot_response)
else:
await update.message.reply_text("I'm not sure how to respond to that. 🤔")
# Generate random percentages for the URL
innovation = random.randint(1, 100)
technology = random.randint(1, 100)
science = random.randint(1, 100)
# Prepare the promotional message
dream_city_link = (
f"Descubre la ciudad de tus sueños! Haz clic en el enlace para explorar una ciudad matemática generada en VR "
f"que podría reflejar tus aspiraciones urbanas: https://app.polisplexity.tech/city/digital/twin/dream/?"
f"innovation={innovation}&technology={technology}&science={science}"
)
# Send the promotional message in a separate follow-up message
await update.message.reply_text(dream_city_link)
except Exception as e:
logger.error(f"Error while processing the message: {e}")
await update.message.reply_text("Oops, I encountered an issue. Please try again later. 😓")
def main() -> None:
"""Start the bot."""
application = Application.builder().token("6474402815:AAHCSXM7VKwyj5-lVh8p3365eRQ5Nj94H4I").build()
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("dream_city", dream_city_command))
debate_filter_private = filters.ChatType.PRIVATE & filters.Regex(r'\bmi ciudad de los sueños\b.*:\s*')
application.add_handler(MessageHandler(debate_filter_private, dream_city_command))
application.add_handler(MessageHandler(filters.ChatType.PRIVATE, respond))
# Replace 'your_bot_username' with your bot's username without '@'
bot_username = 'PolisplexityBot'
mention_pattern = fr'@{bot_username}\b'
# Use the regex filter to check for mentions of the bot
mention_filter = filters.Regex(mention_pattern)
debate_filter_groups = filters.ChatType.GROUPS & filters.Regex(r'\bmi ciudad de los sueños\b.*:\s*')
application.add_handler(MessageHandler(mention_filter & debate_filter_groups, dream_city_command))
application.add_handler(MessageHandler(mention_filter & filters.ChatType.GROUPS, respond))
# application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, respond))
application.add_handler(MessageHandler(filters.LOCATION, handle_location))
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()

3
pxy_bots/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

6
pxy_bots/urls.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import path
from .views import telegram_webhook
urlpatterns = [
path('webhook/<str:bot_name>/', telegram_webhook, name='telegram_webhook'),
]

20
pxy_bots/utils.py Normal file
View File

@ -0,0 +1,20 @@
from .models import TelegramBot
def get_bot_token(bot_name: str) -> str:
"""
Retrieve the Telegram bot token by bot name.
Args:
bot_name (str): The name of the bot to fetch.
Returns:
str: The Telegram bot token.
Raises:
ValueError: If no active bot with the given name is found.
"""
try:
bot = TelegramBot.objects.get(name=bot_name, is_active=True)
return bot.token
except TelegramBot.DoesNotExist:
raise ValueError(f"No active bot found with the name '{bot_name}'")

69
pxy_bots/views.py Normal file
View File

@ -0,0 +1,69 @@
import json
from telegram import Update, Bot
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from asgiref.sync import sync_to_async
from .models import TelegramBot
from pxy_langchain.services import LangchainAIService
from .handlers import dream_city_command, start, help_command, handle_location
import logging
logger = logging.getLogger(__name__)
@csrf_exempt
async def telegram_webhook(request, bot_name):
"""
Webhook view that handles Telegram updates asynchronously and only uses LangChain.
"""
try:
logger.info(f"Webhook called for bot: {bot_name}")
# Step 1: Fetch the bot instance asynchronously
try:
bot_instance = await sync_to_async(TelegramBot.objects.get)(name=bot_name, is_active=True)
logger.info(f"Loaded bot configuration: {bot_instance}")
except TelegramBot.DoesNotExist:
logger.error(f"Bot '{bot_name}' not found or inactive.")
return JsonResponse({"error": f"Bot '{bot_name}' not found."}, status=400)
# Step 2: Ensure the bot has a LangChain assistant
if not bot_instance.assistant:
logger.error(f"No assistant configured for bot '{bot_name}'.")
return JsonResponse({"error": "Assistant not configured."}, status=400)
# Step 3: Process POST request from Telegram
if request.method == "POST":
try:
request_body = json.loads(request.body.decode("utf-8"))
update = Update.de_json(request_body, Bot(token=bot_instance.token))
logger.info(f"Update received: {update}")
except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON: {e}")
return JsonResponse({"error": "Invalid JSON payload"}, status=400)
# Step 4: Route commands to the appropriate handlers
if update.message:
if update.message.text == "/start":
await start(update)
elif update.message.text == "/help":
await help_command(update)
elif update.message.text == "/dream_city":
await dream_city_command(update)
elif update.message.location:
await handle_location(update)
else:
# Step 5: Process AI-generated response using LangChain
assistant_instance = await sync_to_async(LangchainAIService)(bot_instance.assistant)
bot_response = await sync_to_async(assistant_instance.generate_response)(update.message.text)
# Step 6: Send the response back to Telegram
await update.message.reply_text(bot_response)
return JsonResponse({"status": "ok"})
logger.warning("Received non-POST request")
return JsonResponse({"error": "Invalid request method"}, status=400)
except Exception as e:
logger.error(f"Error in webhook: {e}")
return JsonResponse({"error": f"Unexpected error: {str(e)}"}, status=500)

@ -0,0 +1 @@
Subproject commit bf4767d280cb31a73b62ebb6ff912c604423de52

0
pxy_cr/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
pxy_cr/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
pxy_cr/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PxyCrConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pxy_cr"

View File

Binary file not shown.

3
pxy_cr/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
pxy_cr/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
pxy_cr/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
pxy_de/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

36
pxy_de/admin.py Normal file
View File

@ -0,0 +1,36 @@
from django.contrib import admin
from .models import PipelineStep, PipelineRun, PipelineProductRun
class PipelineStepAdmin(admin.ModelAdmin):
list_display = ('name', 'description') # Specify the fields to display
class PipelineProductRunAdmin(admin.ModelAdmin):
list_display = ('pipeline_run', 'step', 'status', 'date')
list_filter = ('status', 'step') # Add filtering by status and step
search_fields = ('pipeline_run__run_identifier', 'step__name') # Enable search by pipeline_run identifier and step name
# Register your models with custom ModelAdmin
admin.site.register(PipelineStep, PipelineStepAdmin)
admin.site.register(PipelineProductRun, PipelineProductRunAdmin)
# pxy_de/admin.py (or wherever your PipelineRun admin is)
from django.contrib import messages
from pxy_de.pipelines.pxy_products.run_products import run_pipeline
def run_pipeline_action(modeladmin, request, queryset):
try:
run_pipeline()
messages.success(request, "Pipeline executed successfully.")
except Exception as e:
messages.error(request, f"Pipeline failed to execute: {str(e)}")
run_pipeline_action.short_description = "Run Products Pipeline"
class PipelineRunAdmin(admin.ModelAdmin):
list_display = ['run_identifier', 'overall_status', 'date']
ordering = ['-date']
actions = [run_pipeline_action]
admin.site.register(PipelineRun, PipelineRunAdmin)

6
pxy_de/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PxyDeConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pxy_de"

View File

@ -0,0 +1,85 @@
# Generated by Django 5.0.3 on 2024-03-16 23:56
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="PipelineRun",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"run_identifier",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
(
"overall_status",
models.CharField(blank=True, max_length=20, null=True),
),
("date", models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name="PipelineStep",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("description", models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name="PipelineProductRun",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("status", models.CharField(max_length=10)),
("date", models.DateTimeField(auto_now_add=True)),
(
"pipeline_run",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="product_runs",
to="pxy_de.pipelinerun",
),
),
(
"step",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pxy_de.pipelinestep",
),
),
],
),
]

View File

Binary file not shown.

30
pxy_de/models.py Normal file
View File

@ -0,0 +1,30 @@
# pxy_de/pipelines/models.py
import uuid
from django.db import models
class PipelineStep(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True)
def __str__(self):
return self.name
class PipelineRun(models.Model):
run_identifier = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
overall_status = models.CharField(max_length=20, blank=True, null=True)
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.run_identifier)
class PipelineProductRun(models.Model):
pipeline_run = models.ForeignKey(PipelineRun, on_delete=models.CASCADE, related_name='product_runs')
step = models.ForeignKey(PipelineStep, on_delete=models.CASCADE)
status = models.CharField(max_length=10)
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.pipeline_run.run_identifier} - {self.step.name}"

View File

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More