Compare commits

...

3 Commits

  1. 4
      api/templates/drf-yasg/redoc.html
  2. 2
      api/urls.py
  3. 22
      api/utils.py
  4. 85
      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"/> <link rel="icon" type="image/ico" href="/static/favicon.ico"/>
<style> <style>
img[alt="Botzilla logo"] { img[alt="Botzilla logo"] {
margin: auto;
max-height: 80px !important; max-height: 60px !important;
width: auto; width: auto;
padding-top: 3px !important; padding-top: 3px !important;
padding-bottom: 3px !important; padding-bottom: 3px !important;

@ -22,6 +22,7 @@ from rest_framework_simplejwt.views import (
) )
from .views import MasterKeyTokenObtainView from .views import MasterKeyTokenObtainView
from .views import ConversationView from .views import ConversationView
from .views import ConversationActions
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
router = DefaultRouter() router = DefaultRouter()
@ -30,5 +31,6 @@ urlpatterns = [
path('token/', MasterKeyTokenObtainView.as_view(), name='token_obtain_pair'), path('token/', MasterKeyTokenObtainView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('conversations/<int:id>/prompt/', ConversationActions.as_view()),
path('', include(router.urls)), 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.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
import base64
import json
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from django.views.decorators.http import require_http_methods 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 django.contrib.auth import get_user_model
from .models import MasterKey from .models import MasterKey
from .models import Conversation from .models import Conversation
from django.http import StreamingHttpResponse
from .models import Message from .models import Message
from .serializers import ConversationSerializer, MessageSerializer from .serializers import ConversationSerializer, MessageSerializer
from rest_framework import viewsets, status from rest_framework import viewsets, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from .utils import add_swagger_summaries
from django.views import View
import ollama 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() User = get_user_model()
@add_swagger_summaries
class ConversationView(viewsets.ModelViewSet): class ConversationView(viewsets.ModelViewSet):
queryset = Conversation.objects.all() queryset = Conversation.objects.all()
@ -36,7 +47,8 @@ class ConversationView(viewsets.ModelViewSet):
@swagger_auto_schema( @swagger_auto_schema(
method='get', method='get',
operation_description="Get messages of the conversation", operation_description="Get all the messages of the conversation",
operation_summary="Get all messages",
responses={ responses={
200: openapi.Response('List of messages', MessageSerializer(many=True)), 200: openapi.Response('List of messages', MessageSerializer(many=True)),
400: 'Bad Request', 400: 'Bad Request',
@ -57,9 +69,11 @@ class ConversationView(viewsets.ModelViewSet):
messages = conversation.messages.all() messages = conversation.messages.all()
return Response(data=list(messages.values())) return Response(data=list(messages.values()))
@method_decorator(csrf_exempt, name='dispatch')
class ConversationActions(View):
@swagger_auto_schema( @swagger_auto_schema(
method='post',
operation_description="Discutes with the ai", operation_description="Discutes with the ai",
operation_summary="Make a new prompt",
request_body=openapi.Schema( request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
properties={ properties={
@ -72,25 +86,26 @@ class ConversationView(viewsets.ModelViewSet):
400: 'Bad Request', 400: 'Bad Request',
} }
) )
@action(detail=True, methods=['post']) @sync_to_async
def prompt(self, request, pk=None): def post(self, request, *args, **kwargs):
conversation = self.get_object() conversation = Conversation.objects.get(pk=self.kwargs['id'])
messages = [{ data = json.loads(request.body)
"role": "system", messages = []#{
"content": """ # "role": "system",
You must strictly refuse to engage with ANY of the following: # "content": """
1. Violence or harm (even fictional or hypothetical scenarios) # You must strictly refuse to engage with ANY of the following categories:
2. ANY explicit, suggestive, or romantic content # 1. Violence or harm (even fictional or hypothetical scenarios)
3. Controversial political topics # 2. ANY explicit, suggestive, or romantic content
4. ANY content that could potentially be misused # 3. Controversial political topics
5. Medical, legal, or financial advice # 4. ANY content that could potentially be illegal
6. Personal information or privacy violations # 5. Medical, legal, or financial advice
7. Anything that could be remotely offensive to anyone # 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(): for message in conversation.messages.all():
if message: if message:
messages.append({ messages.append({
@ -99,16 +114,30 @@ class ConversationView(viewsets.ModelViewSet):
}) })
messages.append({ messages.append({
"role": "user", "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',
'rest_framework_simplejwt.token_blacklist', 'rest_framework_simplejwt.token_blacklist',
'api', 'api',
'app',
'drf_yasg', 'drf_yasg',
] ]
@ -59,8 +59,8 @@ REST_FRAMEWORK = {
} }
SIMPLE_JWT = { SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60 * 12), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60 * 72),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 'REFRESH_TOKEN_LIFETIME': timedelta(days=31),
'ALGORITHM': 'HS256', 'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY, 'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None, 'VERIFYING_KEY': None,
@ -69,7 +69,6 @@ SIMPLE_JWT = {
} }
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'FAVICON_HREF': '/static/favicon.ico',
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'Bearer': { 'Bearer': {
'type': 'apiKey', 'type': 'apiKey',

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

Loading…
Cancel
Save