Writing a chatbot with OpenAI’s Assistant API

bgaudino

Posted by bgaudino

django Python

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.

Return to Articles & Guides