Compare commits

...

3 Commits

  1. 4
      api/templates/drf-yasg/redoc.html
  2. 2
      api/urls.py
  3. 22
      api/utils.py
  4. 87
      api/views.py
  5. 0
      app/__init__.py
  6. 3
      app/admin.py
  7. 6
      app/apps.py
  8. 0
      app/migrations/__init__.py
  9. 3
      app/models.py
  10. 113
      app/templates/index.html
  11. 3
      app/tests.py
  12. 9
      app/urls.py
  13. 9
      app/views.py
  14. 36
      botzilla/docs.py
  15. 7
      botzilla/settings.py
  16. 22
      botzilla/urls.py

@ -4,8 +4,8 @@
<link rel="icon" type="image/ico" href="/static/favicon.ico"/>
<style>
img[alt="Botzilla logo"] {
max-height: 80px !important;
margin: auto;
max-height: 60px !important;
width: auto;
padding-top: 3px !important;
padding-bottom: 3px !important;

@ -22,6 +22,7 @@ from rest_framework_simplejwt.views import (
)
from .views import MasterKeyTokenObtainView
from .views import ConversationView
from .views import ConversationActions
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
@ -30,5 +31,6 @@ 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('conversations/<int:id>/prompt/', ConversationActions.as_view()),
path('', include(router.urls)),
]

@ -0,0 +1,22 @@
from drf_yasg.utils import swagger_auto_schema
def add_swagger_summaries(viewset_class):
"""Add standard swagger summaries to a ModelViewSet class"""
model_name = viewset_class.serializer_class.Meta.model.__name__.lower()
# Apply decorators to the class methods
for action, template in {
'list': f"List all {model_name}s",
'create': f"Create new {model_name}",
'retrieve': f"Get specific {model_name}",
'update': f"Update {model_name} completely",
'partial_update': f"Update {model_name} partially",
'destroy': f"Delete {model_name}",
}.items():
if hasattr(viewset_class, action):
method = getattr(viewset_class, action)
if not hasattr(method, '_swagger_auto_schema'):
setattr(viewset_class, action,
swagger_auto_schema(operation_summary=template)(method))
return viewset_class

@ -1,6 +1,8 @@
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
import base64
import json
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken
from django.views.decorators.http import require_http_methods
@ -11,15 +13,24 @@ from django.utils import timezone
from django.contrib.auth import get_user_model
from .models import MasterKey
from .models import Conversation
from django.http import StreamingHttpResponse
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
from .utils import add_swagger_summaries
from django.views import View
import ollama
from asgiref.sync import sync_to_async
from django.utils.decorators import method_decorator
import asyncio
from django.views.decorators.csrf import csrf_exempt
User = get_user_model()
@add_swagger_summaries
class ConversationView(viewsets.ModelViewSet):
queryset = Conversation.objects.all()
@ -36,7 +47,8 @@ class ConversationView(viewsets.ModelViewSet):
@swagger_auto_schema(
method='get',
operation_description="Get messages of the conversation",
operation_description="Get all the messages of the conversation",
operation_summary="Get all messages",
responses={
200: openapi.Response('List of messages', MessageSerializer(many=True)),
400: 'Bad Request',
@ -57,9 +69,11 @@ class ConversationView(viewsets.ModelViewSet):
messages = conversation.messages.all()
return Response(data=list(messages.values()))
@method_decorator(csrf_exempt, name='dispatch')
class ConversationActions(View):
@swagger_auto_schema(
method='post',
operation_description="Discutes with the ai",
operation_summary="Make a new prompt",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
@ -72,43 +86,58 @@ class ConversationView(viewsets.ModelViewSet):
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
@sync_to_async
def post(self, request, *args, **kwargs):
conversation = Conversation.objects.get(pk=self.kwargs['id'])
data = json.loads(request.body)
messages = []#{
# "role": "system",
# "content": """
# You must strictly refuse to engage with ANY of the following categories:
# 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 illegal
# 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?"
"""
}]
# 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", "")
"content": data['content'] or ""
})
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']
})
Message(role="user", content=data['content'] or "", conversation=conversation).save()
ai_message = Message(role="assistant", content="", conversation=conversation)
@sync_to_async
def save_message():
ai_message.save()
async def chat_event_stream():
message = ""
try:
stream = ollama.chat(model="llama2-uncensored", messages=messages, stream=True)
for chunk in stream:
message = chunk['message']['content']
#print(message, base64.b64encode(message.encode("utf-8")))
ai_message.content += message
yield f"{base64.b64encode(message.encode("utf-8")).decode("utf-8")}"
finally:
await save_message()
response = StreamingHttpResponse(chat_event_stream(), content_type='text/event-stream')
response['Cache-Control'] = 'no-cache'
response['X-Accel-Buffering'] = 'no'
return response

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

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

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

@ -0,0 +1,113 @@
<!-- chat/templates/chat/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js" integrity="sha512-rPuOZPx/WHMHNx2RoALKwiCDiDrCo4ekUctyTYKzBo8NGA79NcTW2gfrbcCL2RYL7RdjX2v9zR0fKyI4U4kPew==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#chat-container {
border: 1px solid #ccc;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
#prompt-input {
width: 100%;
}
#response-container {
min-height: 100px;
border: 1px solid #eee;
color: black;
padding: 10px;
margin-top: 10px;
border-radius: 5px;
}
input {
padding: 8px;
width: 70%;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>Simple Chat App</h1>
<div id="chat-container">
<h3>Enter your message:</h3>
<textarea type="text" id="prompt-input" placeholder="Type your message..." value="hello">
</textarea>
<button onclick="sendPrompt()">Send</button>
<div id="response-container">
<div id="response-text">Response will appear here...</div>
</div>
</div>
<script>
const responseText = document.querySelector('#response-text');
// Function to send the prompt and handle streaming response
responseText.innerHTML = marked.parse("");
async function sendPrompt() {
const promptInput = document.getElementById('prompt-input');
const content = promptInput.value;
// Clear previous response
responseText.textContent = '';
function base64ToUtf8(bytes) {
// Decode UTF-8 bytes to a JavaScript string
return (new TextDecoder()).decode(bytes, { stream: true });
}
try {
// Make the fetch request
const response = await fetch('/api/conversations/14/prompt/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content }),
});
// Handle the event stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function readChunk() {
reader.read().then(({ done, value }) => {
if (done) return;
console.log(value);
buffer += atob((new TextDecoder()).decode(value, { stream: true }))
console.log(buffer)
responseText.innerHTML = marked.parse(buffer);
readChunk();
});
}
readChunk();
} catch (error) {
console.error('Error:', error);
responseText.textContent = `Error: ${error.message}`;
}
}
</script>
</body>
</html>

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

@ -0,0 +1,9 @@
# conversation_app/urls.py
from django.contrib import admin
from django.urls import path, include
from app import views
urlpatterns = [
path('', views.index, name='index'),
]

@ -0,0 +1,9 @@
from django.shortcuts import render
import json
import time
def index(request):
"""
Render the main chat page
"""
return render(request, 'index.html')

@ -0,0 +1,36 @@
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.views import get_schema_view
from rest_framework import permissions
class SubTagSchemaGenerator(OpenAPISchemaGenerator):
def get_schema(self, request=None, public=False):
schema = super().get_schema(request, public)
# Add x-tagGroups extension for ReDoc
schema['x-tagGroups'] = [
{
"name": "v1",
"tags": ["token", "conversations"]
}
]
return schema
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],
generator_class=SubTagSchemaGenerator
)

@ -42,7 +42,7 @@ INSTALLED_APPS = [
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'api',
'app',
'drf_yasg',
]
@ -59,8 +59,8 @@ REST_FRAMEWORK = {
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60 * 12),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60 * 72),
'REFRESH_TOKEN_LIFETIME': timedelta(days=31),
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
@ -69,7 +69,6 @@ SIMPLE_JWT = {
}
SWAGGER_SETTINGS = {
'FAVICON_HREF': '/static/favicon.ico',
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',

@ -17,29 +17,11 @@ Including another URLconf
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],
)
from .docs import schema_view
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('app/', include('app.urls')),
path('docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

Loading…
Cancel
Save