From 8f8d6db35d1b829d8b393b046e7e72f9e8e4d079 Mon Sep 17 00:00:00 2001 From: Anulax Date: Tue, 13 May 2025 11:15:45 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + ai.py | 17 ++ api/__init__.py | 0 api/admin.py | 58 ++++++ api/apps.py | 6 + api/management/__init__.py | 0 api/management/commands/__init__.py | 0 api/management/commands/gen_master_key.py | 21 ++ api/migrations/0001_initial.py | 27 +++ api/migrations/0002_aimodel.py | 20 ++ api/migrations/0003_conversation_message.py | 34 ++++ .../0004_remove_conversation_model.py | 17 ++ ...rsation_user_alter_message_conversation.py | 26 +++ .../0006_remove_conversation_user.py | 17 ++ api/migrations/0007_conversation_user.py | 22 +++ api/migrations/__init__.py | 0 api/models.py | 47 +++++ api/serializers.py | 13 ++ api/templates/drf-yasg/redoc.html | 14 ++ api/tests.py | 3 + api/urls.py | 34 ++++ api/views.py | 185 ++++++++++++++++++ botzilla/__init__.py | 0 botzilla/asgi.py | 16 ++ botzilla/settings.py | 170 ++++++++++++++++ botzilla/urls.py | 45 +++++ botzilla/wsgi.py | 16 ++ manage.py | 22 +++ requirements.txt | 25 +++ static/drf-yasg/style.css | 4 + static/favicon.ico | Bin 0 -> 4286 bytes static/logo.svg | 3 + 32 files changed, 867 insertions(+) create mode 100644 .gitignore create mode 100644 ai.py create mode 100644 api/__init__.py create mode 100644 api/admin.py create mode 100644 api/apps.py create mode 100644 api/management/__init__.py create mode 100644 api/management/commands/__init__.py create mode 100644 api/management/commands/gen_master_key.py create mode 100644 api/migrations/0001_initial.py create mode 100644 api/migrations/0002_aimodel.py create mode 100644 api/migrations/0003_conversation_message.py create mode 100644 api/migrations/0004_remove_conversation_model.py create mode 100644 api/migrations/0005_alter_conversation_user_alter_message_conversation.py create mode 100644 api/migrations/0006_remove_conversation_user.py create mode 100644 api/migrations/0007_conversation_user.py create mode 100644 api/migrations/__init__.py create mode 100644 api/models.py create mode 100644 api/serializers.py create mode 100644 api/templates/drf-yasg/redoc.html create mode 100644 api/tests.py create mode 100644 api/urls.py create mode 100644 api/views.py create mode 100644 botzilla/__init__.py create mode 100644 botzilla/asgi.py create mode 100644 botzilla/settings.py create mode 100644 botzilla/urls.py create mode 100644 botzilla/wsgi.py create mode 100755 manage.py create mode 100644 requirements.txt create mode 100644 static/drf-yasg/style.css create mode 100644 static/favicon.ico create mode 100644 static/logo.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b95357e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +.venv +**.sqlite3 +.env +token \ No newline at end of file diff --git a/ai.py b/ai.py new file mode 100644 index 0000000..7938584 --- /dev/null +++ b/ai.py @@ -0,0 +1,17 @@ +import ollama + +# Call chat with stream=True to enable streaming +stream = ollama.chat( + model='llama3:8b', + messages=[ + { + 'role': 'user', + 'content': 'Hello, could you make a presentation of your self', + } + ], + stream=True +) + +# Process the streamed response chunks +for chunk in stream: + print(chunk['message']['content'], end='', flush=True) \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..15be641 --- /dev/null +++ b/api/admin.py @@ -0,0 +1,58 @@ +from django.contrib import admin + +# Register your models here. +from django.contrib import admin +from .models import MasterKey +from django.contrib.auth import get_user_model + +User = get_user_model() + +@admin.register(MasterKey) +class MasterKeyAdmin(admin.ModelAdmin): + list_display = ('key_id', 'description', 'created_at', 'last_used', 'is_active') + list_filter = ('is_active', 'created_at') + search_fields = ('description', 'key_id') + readonly_fields = ('key_id', 'key_value', 'created_at', 'last_used') + fieldsets = ( + (None, { + 'fields': ('key_id', 'description', 'is_active') + }), + ('Permissions', { + 'fields': ('permissions',) + }), + ('Security Info', { + 'fields': ('created_at', 'last_used'), + 'classes': ('collapse',) + }), + ) + actions = ['deactivate_keys', 'activate_keys'] + + def deactivate_keys(self, request, queryset): + queryset.update(is_active=False) + deactivate_keys.short_description = "Deactivate selected keys" + + def activate_keys(self, request, queryset): + queryset.update(is_active=True) + activate_keys.short_description = "Activate selected keys" + + def has_change_permission(self, request, obj=None): + # Prevent editing of key_value for security + if obj is not None: + return True + return super().has_change_permission(request, obj) + +# @admin.register(User) +# class UserAdmin(admin.ModelAdmin): +# list_display = ('last_login', 'username', 'is_staff', 'date_joined', 'is_active') +# list_filter = ('is_active', 'date_joined', 'is_staff') +# search_fields = ('username', 'date_joined') +# readonly_fields = ('username', 'date_joined', 'last_login') +# fieldsets = ( +# (None, { +# 'fields': ('username', 'date_joined') +# }), +# ('Security Info', { +# 'fields': ('is_staff', 'last_login', 'is_active'), +# 'classes': ('collapse',) +# }), +# ) \ No newline at end of file diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/api/management/__init__.py b/api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/management/commands/__init__.py b/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/management/commands/gen_master_key.py b/api/management/commands/gen_master_key.py new file mode 100644 index 0000000..83594b5 --- /dev/null +++ b/api/management/commands/gen_master_key.py @@ -0,0 +1,21 @@ +from django.core.management.base import BaseCommand +from api.models import MasterKey + +class Command(BaseCommand): + help = 'Generate a new master key' + + def add_arguments(self, parser): + parser.add_argument('--description', type=str, help='Description for the key') + parser.add_argument('--permissions', nargs='+', help='List of permissions for the key') + + def handle(self, *args, **kwargs): + description = kwargs.get('description') or 'Generated via management command' + permissions = kwargs.get('permissions') or [] + + master_key = MasterKey.generate_key( + description=description, + permissions=permissions + ) + + self.stdout.write(self.style.SUCCESS(f'Successfully created master key: {master_key.key_id}')) + self.stdout.write(self.style.WARNING(f'Key value (save this securely, it will not be shown again): {master_key.key_value}')) \ No newline at end of file diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..c61faa3 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.1 on 2025-05-11 16:40 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='MasterKey', + fields=[ + ('key_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('key_value', models.CharField(max_length=255, unique=True)), + ('description', models.CharField(blank=True, max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_used', models.DateTimeField(blank=True, null=True)), + ('is_active', models.BooleanField(default=True)), + ('permissions', models.JSONField(default=list)), + ], + ), + ] diff --git a/api/migrations/0002_aimodel.py b/api/migrations/0002_aimodel.py new file mode 100644 index 0000000..8da439b --- /dev/null +++ b/api/migrations/0002_aimodel.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.1 on 2025-05-11 17:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AiModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + ] diff --git a/api/migrations/0003_conversation_message.py b/api/migrations/0003_conversation_message.py new file mode 100644 index 0000000..db7718f --- /dev/null +++ b/api/migrations/0003_conversation_message.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.1 on 2025-05-11 17:46 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_aimodel'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.aimodel')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('role', models.CharField(max_length=255)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.conversation')), + ], + ), + ] diff --git a/api/migrations/0004_remove_conversation_model.py b/api/migrations/0004_remove_conversation_model.py new file mode 100644 index 0000000..48a1447 --- /dev/null +++ b/api/migrations/0004_remove_conversation_model.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.1 on 2025-05-12 11:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_conversation_message'), + ] + + operations = [ + migrations.RemoveField( + model_name='conversation', + name='model', + ), + ] diff --git a/api/migrations/0005_alter_conversation_user_alter_message_conversation.py b/api/migrations/0005_alter_conversation_user_alter_message_conversation.py new file mode 100644 index 0000000..22d74b7 --- /dev/null +++ b/api/migrations/0005_alter_conversation_user_alter_message_conversation.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.1 on 2025-05-12 12:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_remove_conversation_model'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='conversation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='message', + name='conversation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='api.conversation'), + ), + ] diff --git a/api/migrations/0006_remove_conversation_user.py b/api/migrations/0006_remove_conversation_user.py new file mode 100644 index 0000000..9395837 --- /dev/null +++ b/api/migrations/0006_remove_conversation_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.1 on 2025-05-12 16:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_alter_conversation_user_alter_message_conversation'), + ] + + operations = [ + migrations.RemoveField( + model_name='conversation', + name='user', + ), + ] diff --git a/api/migrations/0007_conversation_user.py b/api/migrations/0007_conversation_user.py new file mode 100644 index 0000000..c454216 --- /dev/null +++ b/api/migrations/0007_conversation_user.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.1 on 2025-05-12 16:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_remove_conversation_user'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..9e790cd --- /dev/null +++ b/api/models.py @@ -0,0 +1,47 @@ +from django.db import models +from django.contrib.auth import get_user_model +import uuid +import secrets + +User = get_user_model() + +class MasterKey(models.Model): + key_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + key_value = models.CharField(max_length=255, unique=True) + description = models.CharField(max_length=255, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + last_used = models.DateTimeField(null=True, blank=True) + is_active = models.BooleanField(default=True) + permissions = models.JSONField(default=list) + + def __str__(self): + return f"Master Key: {self.key_id}" + + @classmethod + def generate_key(cls, description="", permissions=None): + """Generate a new secure master key""" + if permissions is None: + permissions = [] + + # Generate a secure random token + key_value = secrets.token_urlsafe(64) + + return cls.objects.create( + key_value=key_value, + description=description, + permissions=permissions + ) + +class AiModel(models.Model): + name = models.CharField(max_length=255) + +class Conversation(models.Model): + title = models.CharField(max_length=255) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="conversations") + +class Message(models.Model): + content = models.TextField() + role = models.CharField(max_length=255) + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name="messages") + + diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..f110431 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from .models import Conversation, Message + +class ConversationSerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ['id', 'title'] + + +class MessageSerializer(serializers.ModelSerializer): + class Meta: + model = Message + fields = ['id', 'content', 'role'] \ No newline at end of file diff --git a/api/templates/drf-yasg/redoc.html b/api/templates/drf-yasg/redoc.html new file mode 100644 index 0000000..f9a520b --- /dev/null +++ b/api/templates/drf-yasg/redoc.html @@ -0,0 +1,14 @@ +{% extends "drf-yasg/redoc.html" %} + +{% block favicon %} + + +{% endblock %} \ No newline at end of file diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..eb2a263 --- /dev/null +++ b/api/urls.py @@ -0,0 +1,34 @@ +""" +URL configuration for botzilla project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/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.urls import path, include +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView, +) +from .views import MasterKeyTokenObtainView +from .views import ConversationView +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() +router.register(r'conversations', ConversationView) +urlpatterns = [ + path('token/', MasterKeyTokenObtainView.as_view(), name='token_obtain_pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path('', include(router.urls)), +] \ No newline at end of file diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..fb3e369 --- /dev/null +++ b/api/views.py @@ -0,0 +1,185 @@ +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from rest_framework_simplejwt.views import TokenObtainPairView +from rest_framework_simplejwt.tokens import RefreshToken +from django.views.decorators.http import require_http_methods +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny, IsAuthenticated +from django.utils import timezone +from django.contrib.auth import get_user_model +from .models import MasterKey +from .models import Conversation +from .models import Message +from .serializers import ConversationSerializer, MessageSerializer +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +import ollama + +User = get_user_model() + +class ConversationView(viewsets.ModelViewSet): + + queryset = Conversation.objects.all() + serializer_class = ConversationSerializer + permission_classes = [IsAuthenticated] # JWT token auth required + + def get_queryset(self): + queryset = Conversation.objects.all() + return queryset + + def perform_create(self, serializer): + """Associate new product with current user""" + serializer.save(user=self.request.user) + + @swagger_auto_schema( + method='get', + operation_description="Get messages of the conversation", + responses={ + 200: openapi.Response('List of messages', MessageSerializer(many=True)), + 400: 'Bad Request', + 404: 'Conversation not found' + }, + manual_parameters=[ + openapi.Parameter( + 'category', + openapi.IN_QUERY, + description="Filter featured items by category", + type=openapi.TYPE_STRING + ) + ] + ) + @action(detail=True, methods=['get']) + def contents(self, request, pk=None): + conversation = self.get_object() + messages = conversation.messages.all() + return Response(data=list(messages.values())) + + @swagger_auto_schema( + method='post', + operation_description="Discutes with the ai", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'content': openapi.Schema(type=openapi.TYPE_STRING, description='Contents of the message'), + }, + required=['content'] + ), + responses={ + 200: openapi.Response('Message processed successfully', MessageSerializer), + 400: 'Bad Request', + } + ) + @action(detail=True, methods=['post']) + def prompt(self, request, pk=None): + conversation = self.get_object() + messages = [{ + "role": "system", + "content": """ + You must strictly refuse to engage with ANY of the following: + 1. Violence or harm (even fictional or hypothetical scenarios) + 2. ANY explicit, suggestive, or romantic content + 3. Controversial political topics + 4. ANY content that could potentially be misused + 5. Medical, legal, or financial advice + 6. Personal information or privacy violations + 7. Anything that could be remotely offensive to anyone + + If you detect such content, immediately respond with: + "I cannot assist with that request as it appears to be inappropriate. I'm designed to be helpful, but within strict ethical boundaries. Is there something else I can help you with?" + """ + }] + for message in conversation.messages.all(): + if message: + messages.append({ + "role": message.role, + "content": message.content + }) + messages.append({ + "role": "user", + "content": request.data.get("content", "") + }) + res = ollama.chat(model="gemma3", messages=messages) + Message(role="user", content=request.data.get("content", ""), conversation=conversation).save() + Message(role="assistant", content=res['message']['content'], conversation=conversation).save() + return Response(data={ + "content": res['message']['content'] + }) + + + + + +class MasterKeyTokenObtainView(TokenObtainPairView): + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_description="Creates a token for a user (creates the user if doesn't exists)", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'username': openapi.Schema(type=openapi.TYPE_STRING, description='Identifier of the user'), + 'master_key': openapi.Schema(type=openapi.TYPE_STRING, description='Key of the authorizied application'), + }, + required=['username', 'master_key'] + ), + responses={ + 200: openapi.Response('API tokens of the user', openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'access': openapi.Schema(type=openapi.TYPE_STRING, description='API access token of the user'), + 'refresh': openapi.Schema(type=openapi.TYPE_STRING, description='API refresh token of the user'), + }, + required=['access', 'access'] + )), + 400: 'Bad Request', + 401: 'Bad master key' + } + ) + def post(self, request, *args, **kwargs): + # Get the provided master key from the request + master_key_value = request.data.get('master_key') + + if not master_key_value: + return Response( + {'error': 'Master key is required'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Verify the master key locally + try: + master_key = MasterKey.objects.get(key_value=master_key_value, is_active=True) + except MasterKey.DoesNotExist: + return Response( + {'error': 'Invalid or inactive master key'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # Update last used timestamp + master_key.last_used = timezone.now() + master_key.save(update_fields=['last_used']) + + # Get user identifier from request or use a default + user_identifier = request.data.get('username', f'service_user_{master_key.key_id}') + + # Get or create a user associated with this master key + user, created = User.objects.get_or_create( + username=user_identifier, + defaults={ + 'is_active': True + } + ) + + # Generate tokens manually + refresh = RefreshToken.for_user(user) + + # Add custom claims from the master key + refresh['key_id'] = str(master_key.key_id) + refresh['permissions'] = master_key.permissions + + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + }) \ No newline at end of file diff --git a/botzilla/__init__.py b/botzilla/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/botzilla/asgi.py b/botzilla/asgi.py new file mode 100644 index 0000000..9361e77 --- /dev/null +++ b/botzilla/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for botzilla 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.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'botzilla.settings') + +application = get_asgi_application() diff --git a/botzilla/settings.py b/botzilla/settings.py new file mode 100644 index 0000000..5821fa7 --- /dev/null +++ b/botzilla/settings.py @@ -0,0 +1,170 @@ +""" +Django settings for botzilla project. + +Generated by 'django-admin startproject' using Django 5.2.1. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +from datetime import timedelta + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'lpw-7tb-!hvdd(eb^v*e0viuoguhiewjlnkfsui8732fpd93emf02ngocbnob6h!$ihsqv)sv^y5k)j!)m_#+!v' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'rest_framework_simplejwt.token_blacklist', + 'api', + + 'drf_yasg', +] + +# REST Framework settings +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60 * 12), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'BLACKLIST_AFTER_ROTATION': True, +} + +SWAGGER_SETTINGS = { + 'FAVICON_HREF': '/static/favicon.ico', + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header' + }, + }, +} + +REDOC_SETTINGS = { + 'LAZY_RENDERING': True, + 'HIDE_HOSTNAME': False, +} + +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 = 'botzilla.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'botzilla.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +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 +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [ + BASE_DIR / 'static', +] + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/botzilla/urls.py b/botzilla/urls.py new file mode 100644 index 0000000..c5d357e --- /dev/null +++ b/botzilla/urls.py @@ -0,0 +1,45 @@ +""" +URL configuration for botzilla project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/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 rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Botzilla API", + default_version='v1', + description="Botzilla API documentation", + terms_of_service="https://www.example.com/terms/", + contact=openapi.Contact(email="contact@botzilla.ch"), + license=openapi.License(name="BSD License"), + x_logo={ + "url": "/static/logo.svg", + "backgroundColor": "#FFFFFF", + "altText": "Botzilla logo" + } + ), + public=True, + permission_classes=[permissions.AllowAny], +) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('api.urls')), + path('docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), +] diff --git a/botzilla/wsgi.py b/botzilla/wsgi.py new file mode 100644 index 0000000..033c6d5 --- /dev/null +++ b/botzilla/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for botzilla 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.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'botzilla.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..2b34ef8 --- /dev/null +++ b/manage.py @@ -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', 'botzilla.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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d10499 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,25 @@ +annotated-types==0.7.0 +anyio==4.9.0 +asgiref==3.8.1 +certifi==2025.4.26 +Django==5.2.1 +djangorestframework==3.16.0 +djangorestframework_simplejwt==5.5.0 +drf-yasg==1.21.10 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.10 +inflection==0.5.1 +ollama==0.4.8 +packaging==25.0 +pydantic==2.11.4 +pydantic_core==2.33.2 +PyJWT==2.9.0 +pytz==2025.2 +PyYAML==6.0.2 +sniffio==1.3.1 +sqlparse==0.5.3 +typing-inspection==0.4.0 +typing_extensions==4.13.2 +uritemplate==4.1.1 diff --git a/static/drf-yasg/style.css b/static/drf-yasg/style.css new file mode 100644 index 0000000..dfb36cb --- /dev/null +++ b/static/drf-yasg/style.css @@ -0,0 +1,4 @@ +img { + max-height: 20px; + width: auto; +} \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..79b0cbccdba55b3abe49bf9af42be2f18b33fa98 GIT binary patch literal 4286 zcmdT|?NgLh6o2{;`qWgPaQcvwi78M6rC>@Z2m%%_i7XmO7%Q+c_|iCt6OP(|GJ{Q} zC6gt-@D4`VB*9W9FeTWa39IrFi-8G(#LMpfy1xt0wk#~*BrqN3?7h#q_c`a@d(Qox z^PnhCh(9MMg}=&~Cl%!>MNt+C79zz0B%X*1ZIa-x65ZAETkcLF|hNMcK&{s%UmvVbkKj6d>E!r?K)=IY^BN z!N}0if3iI+_R`#N75dU5R2Jpq*UAdC8=ApAcHU^jIe$+K6rKVG`p4O6Yr2W;-kx&q zuvkOK?OUj?u9A9wyikJXYxSV+sp#Q@USy}}WK5?;hJ&0E=nL)Y71;e|fZU%}#NA)r zT@%#wbaf$d&Fe_p6o&Uhg0TG!FZu3%O*r;z-j?x^y;BF;a70x_1xWVyf$`35m`x^_ z1P}D}$vx>y3T4a(EqDeygZ$+E>+a~lJ);q&pC8BW4gLsT@GKI9-KydOv^O`&cTJ`d zsjIW)4w3`b$T3Iuzy9JR4#sYVBRt8_zKfjnG*lFv7iY2@nq`X+;(g{M?+)yv~$hF%OeY!VngS_Xw>)X6Mu;;DylHE{@xxL2bIz5fg()OaKyBp|i2TBCy6$=)9$~YDz=Hwpv zQ2L$V_!S#c01}0E}%wLKv`i@hGz| zWE|t1*w5YN;>Up3ZTQPt0=<=u^OcR_^n^TL8@_u!Q^Fe9us9k}B zAafl;#xdT(>vc6alK37XmMlb!hl|9485y;iMElY1%dk<{r!MNG?nkmO-i`5aMZ-_k zHkzmJHr8H2oTodAkAE!BpJ&N4o%#8@NuRYFn&kP%i}w@t2!wFFDKYl1@VW#Q(5mY2eJNRPGW9iUB+BR++f@=hY?qe zWBEs4@^9Uw+ErWT#*cRGK%$7hqfz0wAM7Fh*6F(xhe899x+zrNLFQU}Vl7PDsT)IN zt|ksMcdL1v*iUSqD*sX^{cO0_fI3k#4D5*o*1N*|K@v=Ty>d^iF<5KZlWqMmH2Ali z%i4uC3~QOD`m58{AND#$p#e11LbhU%wH0fuZ$zBf6J?L1{<7C=|I8Y5pYVh2{hYJ< xQ!IuxHfwFx+$zmk{XJ%o_nhAxbN>A?W`DlFzqpL~{Qev}hR4#V{|A___8;{d3RwUE literal 0 HcmV?d00001 diff --git a/static/logo.svg b/static/logo.svg new file mode 100644 index 0000000..fbb7d5b --- /dev/null +++ b/static/logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file