Here at Fusionbox, we’ve been thinking a lot about ways to harness AI’s superpowers in our client’s applications. One way to do this is with a chatbot specialized to your business. This can address customers immediately and free employees to work on higher priority items. Read on for how we might build a custom-made chatbot using Django and OpenAI’s Assistant API.
Though the Assistant API is in its beta phase (at the time of this post), it has features that are particularly valuable for client-facing businesses. These include:
- Custom Instructions: Instructions can be tailored to create an assistant that behaves the way you want it to.
- Thread Management: The Assistant API automatically manages conversation histories so that they do not need to be sent with every request.
- Knowledge Retrieval: Upload files and enable Knowledge Retrieval to have your assistant look up data specific to your use case. The Assistant will even cite where it finds the data.
Setup
To set up our chatbot app, install Django and the OpenAI Python SDK:
1 2 | pip install django
pip install openai
|
Then create a django project and app.
1 2 | django-admin startproject config .
django-admin startapp chatbot
|
Next, add your API key and Assistant ID to config/settings.py
. Remember to keep the API key secure and avoid committing it to version control. I recommend using environment variables.
1 2 3 4 | import os
OPEN_AI_API_KEY = os.environ['OPEN_AI_API_KEY']
OPEN_AI_ASSISTANT_ID = os.environ['OPEN_AI_ASSISTANT_ID']
|
Now let's start building our backend.
Backend
In a file called chatbot/forms.py
create a simple form with fields for the user's message and a hidden field to store the thread ID:
1 2 3 4 5 | from django import forms
class ChatForm(forms.Form):
message = forms.CharField(required=True)
thread_id = forms.CharField(required=False, widget=forms.HiddenInput())
|
Next, let’s take a look at chatbot/views.py
where we will instantiate the OpenAI client and define a Django view. We will subclass Django's generic FormView
(which will take care of most of the usual boilerplate) and define custom form_valid
and form_invalid
methods. The form_valid
method is where most of the action will happen. Let's map out what we need to do.
- Create a new thread if one does not already exist
- Add our user's message to the thread
- Generate an assistant response
- Stream the response to the client
Here's what that looks like in code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import json
from django.conf import settings
from django.http import StreamingHttpResponse
from django.views.generic import FormView
from openai import OpenAI
from .forms import ChatForm
client = OpenAI(api_key=settings.OPEN_AI_API_KEY)
class ChatbotView(FormView):
form_class = ChatForm
template_name = 'chatbot.html'
def form_valid(self, form):
thread_id = form.cleaned_data.get('thread_id')
if not thread_id:
thread_id = client.beta.threads.create().id
message = form.cleaned_data['message']
client.beta.threads.messages.create(
thread_id=thread_id,
role='user',
content=message,
)
return StreamingHttpResponse(self.stream_response(thread_id))
def form_invalid(self, form):
data = json.dumps({'form': form.as_div()})
return StreamingHttpResponse([f'event: FormInvalid\ndata: {data}\n\n'])
def stream_response(self, thread_id):
with client.beta.threads.runs.stream(
thread_id=thread_id,
assistant_id=settings.OPEN_AI_ASSISTANT_ID,
) as stream:
for event in stream:
if event.event == 'thread.message.delta':
yield f'data: {event.model_dump_json()}\n\n'
elif event.event == 'thread.run.created':
yield f'event: RunCreated\ndata: {event.model_dump_json()}\n\n'
yield 'event: Done\n\n'
|
Finally, implement JavaScript to handle form submission and display responses dynamically. Here's what a simple implementation might look like. I'm using a library called sse.js which is designed as a drop-in replacement for the EventSource
browser API. This library has useful features not supported by EventSource
such as custom headers and request payloads. After posting our form we can add various event listeners to perform actions such as adding the AI response to the DOM or handling errors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | import {SSE} from 'sse.js';
document.getElementById('chat-form').addEventListener('submit', handleSubmit);
function handleSubmit(e) {
// Prevent reloading of page and get form data
e.preventDefault();
const data = new FormData(e.target);
startLoadingState();
// Add user's message to the DOM
const history = document.getElementById('chat-history');
const userMessage = document.createElement('p');
userMessage.textContent = `User: ${data.get('message')}`;
history.append(userMessage);
// Create the assistant response element and add to DOM
const assistantMessage = document.createElement('p');
assistantMessage.textContent = 'Assistant: ';
history.append(assistantMessage);
// Post the form
const source = new SSE(e.target.action, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name="csrfmiddlewaretoken"]')
.value,
},
payload: data,
});
// Listen for the response and add to the DOM as it's recieved
source.addEventListener('message', function (e) {
const payload = JSON.parse(e.data);
payload.data.delta.content.forEach(
(delta) => (assistantMessage.textContent += delta.text.value)
);
});
// Update the thread id and remove any error messages
source.addEventListener('RunCreated', function (e) {
for (const errors of document.querySelectorAll('.errorlist')) {
errors.remove();
}
const payload = JSON.parse(e.data);
document.getElementById('id_thread_id').value = payload.data.thread_id;
});
// Display error messages
source.addEventListener('FormInvalid', function (e) {
userMessage.remove();
assistantMessage.remove();
const payload = JSON.parse(e.data);
document.getElementById('form-container').innerHTML = payload.form;
endLoadingState();
});
// Close the connection
source.addEventListener('Done', function (e) {
e.source.close();
endLoadingState();
});
}
function startLoadingState() {
const submitButton = document.getElementById('submit-button');
submitButton.disabled = true;
submitButton.value = 'Loading...';
document.getElementById('id_message').value = '';
}
function endLoadingState() {
const submitButton = document.getElementById('submit-button');
submitButton.disabled = false;
submitButton.value = 'Submit';
document.getElementById('id_message').focus();
}
|
Done
With these steps, you've built a functional chatbot that can be seamlessly integrated as a standalone app into any Django project, provided you have an OpenAI account and an assistant configured for your use case.
If you’re interested in incorporating a custom-built AI chatbot into your site, we’d love to assist.