Jordan Savant // Software Architect

Project Space Bus

Goals

  • deeper dive into Python
  • django as a webhost with apache reverse proxy loadbalance and gunicorn/uwsgi
    • forms, models, views, urls etc
  • openai integration

Application

You partake in conversations with various passengers in an intergalactic bus. Each passenger has a backstory, description, motivation and personality - representing an AI agent. The driver is a controller AI who can ensure conversations are in alignment with a standard. Another AI that can set spatial atmosphere with structured data.

Django

  • has a model per actor
    • form to create or edit the actor
    • log?
  • has two views
    • bus screen for interaction
    • edit screens for actors

OpenAI

  • agents per model?

Dev Log

Python venv

https://docs.python.org/3/library/venv.html

A python virtual environment runs on top of the base python os installation and houses its own isolated packages. While the venv is active cli command python will use the same python version as the base python and pip will install packages only within the virtual environment.

These should not be tracked in version control and cannot be moved around the filesystem. Instead they should be recreated as needed.

Get python venv toolset using apt:

apt-get install python3-venv

Setup the venv to run the app in:

python -m venv ./virtual_env

Activate the venv (linux):

source virtual_env/bin/activate

Deactiave the venv (while in venv mode):

deactivate

Delete the venv:

rm -rf ./virtual_env

Recreating the venv:

  1. Use pip to output currently installed packages using pip freeze or pip freeze > requirements.txt
  2. Reinstall those packages with pip install -r requirements.txt

This file should be tracked in the project version control.

Note that using Poetry is best for package and environment dependency management. These create pyproject.toml (similar to a composer.json file in PHP) and a poetry.lock file (similar to the composer.lock file in PHP). Both should be committed for best practices. https://python-poetry.org

Python Coding Standards

https://peps.python.org/pep-0008

Django Installed

Install django (in venv):

python -m pip install Django

Test installation

$ python
>>> import django
>>> print(django.get_version())
5.2.5

New Django Site

$ mkdir [folder]
$ django-admin startproject [project name] [folder]

This created a folder with all scaffolding for the overall application project. A project parents internal apps.

manage.py is essentially the equivalent of Laravel artisan.

Start Dev Server (from folder)

$ python manage.py runserver

The development server automatically reloads Python code for each request as needed. You don’t need to restart the server for code changes to take effect (except when new files are created).

Create the first App within the Project

$ python manage.py startapp [app name]

Wire up routes and views accordingly: https://docs.djangoproject.com/en/5.2/intro/tutorial01/

Wire up database to the settings.py file.

Run default app Migrations. These run based on apps in INSTALLED_APPS in the settings.

$ python manage.py migrate

Create models, add app to installed apps, create migrations, run migrations, add to built in django admin


SpaceBus App Models

# create parent project in folder spacebus_app
django-admin startproject spacebus spacebus_app
cd spacebus_app
# build admin and preinstalled app migrations to local sqlite db
python manage.py migrate

# create sub app in project called thebus to house our bus models and logic
python manage.py startapp thebus

Create the models thebus/models.py:

class Location(models.Model):
    name = models.CharField(max_length=100)
    coordinate_x = models.FloatField(null=True, blank=True)
    coordinate_y = models.FloatField(null=True, blank=True)
    coordinate_z = models.FloatField(null=True, blank=True)

    def __str__(self):
        return f"({self.name}, {self.coordinates})"

class SpaceBus(models.Model):
    name = models.CharField(max_length=100)
    state = models.CharField(max_length=50) # e.g., 'idle', 'in transit', 'docked'
    capacity = models.IntegerField()
    levels = models.IntegerField()
    rows_per_level = models.IntegerField()
    seats_per_row = models.IntegerField()

    coordinate_x = models.FloatField(null=True, blank=True)
    coordinate_y = models.FloatField(null=True, blank=True)
    coordinate_z = models.FloatField(null=True, blank=True)
    last_location = models.ForeignKey(Location, related_name='last_location', on_delete=models.CASCADE, null=True, blank=True)
    next_location = models.ForeignKey(Location, related_name='next_location', on_delete=models.CASCADE, null=True, blank=True)

    def __str__(self):
        return f"{self.name} ({self.state}) heading to {self.next_location.name if self.next_location else 'N/A'}"

class Seat(models.Model):
    bus = models.ForeignKey(SpaceBus, on_delete=models.CASCADE)

    level = models.IntegerField()
    row = models.IntegerField()
    seat_number = models.IntegerField()
    dirtiness = models.FloatField(default=0.0)  # 0.0 (clean) to 1.0 (dirty)

    def __str__(self):
        return f"Bus: {self.bus.name}, Level: {self.level}, Row: {self.row}, Seat: {self.seat_number}"

class Race(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    homeworld = models.ForeignKey(Location, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.name} from {self.homeworld.name}"

class Passenger(models.Model):
    name = models.CharField(max_length=100)
    race = models.OneToOneField(Race, on_delete=models.CASCADE)
    age = models.IntegerField()
    backstory = models.TextField()
    is_user = models.BooleanField(default=False)

    bus = models.ForeignKey(SpaceBus, on_delete=models.CASCADE, null=True, blank=True)
    seat = models.OneToOneField(Seat, on_delete=models.CASCADE, null=True, blank=True)
    destination = models.ForeignKey(Location, on_delete=models.CASCADE, null=True, blank=True)

    def __str__(self):
        return f"{self.name}, {self.race} headed to {self.destination.name if self.destination else 'N/A'}"

Add the app thebus to the list of installed apps for the spacebus project in spacebus/settings.py

The app is thebus.apps.TheBusConfig which is a func defined in thebus/apps.py

Create model migration files with python manage.py makemigrations thebus

See the SQL that will run for the migrations with python manage.py sqlmigrate thebus 0001

See the migration plan before it runs: python manage.py migrate --plan

Run the migrations python manage.py migrate

Interestingly migrations that modify a table actually create a new table with full columns then insert all records from old table into new table then drop old table.

We can use the interactive shell to tinker with models and app: python manage.py shell

SpaceBus App Admin

Create the base superuser with python manage.py createsuperuser

Login at http://localhost:8000/admin/ (with server running)

thebus app models are not managed in admin by default. We must expose models to admin by registering them with in the thebus/admin.py file.

We can edit the admin form field presentation: https://docs.djangoproject.com/en/5.2/intro/tutorial07/

view of admin page showing models

SpaceBus Primitive Views

Focusing on two views:

  • Overall bus that displays it and its passengers
    • Has a form to converse with all passengers
  • Specific passenger view for looking at a passenger
    • Has a form to converse with specific passenger

Add app views at thebus/views.py such as:

from django.shortcuts import render
from django.http import HttpResponse

# https://docs.djangoproject.com/en/5.2/ref/models/querysets/

def index(request):
    buses = SpaceBus.objects
    return HttpResponse("Index of all active buses: %s count" % buses.count(

def bus(request, bus_id):
    bus = SpaceBus.objects.get(id = bus_id)
    return HttpResponse("View bus %s: %s" % (bus_id, bus))

def passenger(request, passenger_id):
    passenger = Passenger.objects.get(id = passenger_id)
    return HttpResponse("View passenger %s: %s" % (passenger_id, passenger))

Then add the url for the view at app/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'), # lists buses
    path('bus/<int:bus_id>', views.bus, name='bus'), # single bus view
    path('passenger/<int:passenger_id>', views.passenger, name='passenger'), # single passenger view
]

Then include the app urls in the main project urls with:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('spacebus/', include("thebus.urls")),
]

At this stage we have our three primitive views that allow us to view the structures we have established.

We want to now extend these into template views with navigation between the various views.

SpaceBus Templating

Templates are put into an app subfolder named templates/thebus.

We created a base template to inherit from for our main html structure: shell.html

<html>
    <head>
        <title>SpaceBus Enterprise</title>
    </head>
    <body>
        {% block body %}
        No Body
        {% endblock %}
    </body>
    {% block postbody %}
    {% endblock %}
</html>

We then updated the views (controllers) to pull data and pass to templates:

from django.shortcuts import render
from django.http import HttpResponse
from .models import *

# https://docs.djangoproject.com/en/5.2/ref/models/querysets/
def index(request):
    buses = SpaceBus.objects.all()
    return render(request, "thebus/index.html", {
        "buses": buses
    })

def bus(request, bus_id):
    bus = SpaceBus.objects.get(id = bus_id)
    passengers = Passenger.objects.filter(bus_id = bus.id)
    return render(request, "thebus/bus.html", {
        "bus": bus,
        "passengers": passengers,
    })

def passenger(request, passenger_id):
    passenger = Passenger.objects.get(id = passenger_id)
    return render(request, "thebus/passenger.html", {
        "passenger": passenger
    })

index.html

{% extends "thebus/shell.html" %}

{% block body %}
Index of all buses:
<ul>
    {% for bus in buses %}
    <li>
        <a href="{% url 'bus' bus.id %}">{{ bus }}</a>
    </li>
    {% endfor %}
</ul>
{% endblock %}

bus.html

{% extends "thebus/shell.html" %}

{% block body %}
Bus {{ bus }}
<ul>
    {% for passenger in passengers %}
    <li>
        <a href="{% url 'passenger' passenger.id %}">{{ passenger }}</a>
    </li>
    {% endfor %}
</ul>
{% endblock %}

passenger.html

{% extends "thebus/shell.html" %}

{% block body %}
Passenger {{ passenger }}
{% endblock %}

At this stage we can navigate in and around the site in a basic way.

index page of the bus listing view

Open AI Integration

We now have a basic Django site with our models which we can manage in the admin, and views which can let us see our models. Its now time to begin integrating OpenAI so that we can have conversations with our passengers.

Our first pass will be to simply add a form to interact with a passenger in a one on one basis. We will take the stored description of the passenger and the backstory and use it as the prompt introduction to ChatGPT whilst also introducing the player from the perspective of the passenger marked as player.

Integrating OpenAI

Obviously we will be integrating with the official OpenAI Python SDK.

https://platform.openai.com/docs/libraries?language=python

I stored my openai key in a local file then bootstrapped a basic prompt into the primary controller to see if it worked:

pip install openai

from openai import OpenAI

def get_openai_key():
    f = open("/home/doomguy/source/spacebus/openai_key")
    key = f.read().strip()
    f.close()
    return key

def index(request):
    # test we can connect and perform a basic prompt
    client = OpenAI(
        api_key = get_openai_key()
    )

    prompt = "Give me a short, inspiring space travel quote."
    response = client.responses.create(
        model = "gpt-5-nano",
        #instructions = "You are a coding assistant that talks like a pirate.",
        input = prompt,
    )

    buses = SpaceBus.objects.all()
    return render(request, "thebus/index.html", {
        "buses": buses,
        "prompt": prompt,
        "response": response.output_text,
    })

It output:

We travel to the stars to discover how far wonder can carry us.

Agentic Flow

In order to move beyond a basic prompting system we need to incorporate two main things:

  1. Conversational history to ensure talking with a passenger doesn't reset on each prompt.
  2. Designating developer instructions for the AI agent to follow so that prompts can come from a roleplay perspective.

Our first iteration will cover the conversational requirement and we will focus on the Passenger view in order to test this. We will create a form on the passenger page that will let us supply a conversation prompt and output the AI response on page POST.

These prompts will need to ensure they retain the previous prompts and responses to ensure the conversation evolves and doesn't reset on each load. We can do this in fews ways using OpenAI:

  1. We supply the input as an array instead of string with each element having a role and the related string. The AI agent will see this sequentially growing array as historical conversation context.
  2. We create a conversation with our initial prompt and supply the conversation ID into successive prompts and allow OpenAI to retain the session state of conversation on their side.
  3. After each prompt and response we take the response ID and supply it as a parameter to the next prompt.

The second option seems the most robust and we will use it.

Alongside this we must also designate "roles" by which to orchestrate the conversation.

  • The developer role allows us to supply prompts instructions on how the AI should behave in the conversation. These instructions supercede any requests of the user.
  • The user role is the roleplaying portion of our user where we supply prompts acting as the player passenger on the bus.
  • The assistant role is the AI agent and are designated only on the response output. These can be resupplied in the conversation model we discussed previously.

Passenger Form

We namespaced the URLs to make the convention properly used by adding app_name = "thebus" to the urls.py file and then for templatized urls we prefix the view name with it, e.g. {% url 'thebus:passenger' ... %}

On the passenger template we add the form to submit to a dedicated view which can submit the user's prompt, will initiate the open ai call and then return and print the response on the page.

The first pass is a basic form that submits the prompt and queries Open AI for a non-conversational response that has an instruction set for a generic alien (not incorporating our alien backstory for the passenger).

Updated passenger.html template:

{% extends "thebus/shell.html" %}

{% block body %}

<p>
    Passenger {{ passenger }}
</p>

<form method="POST" action="{% url 'thebus:passenger' passenger.id %}">
    {% csrf_token %}
    <div>
        Prompt:<br>
        <textarea name="prompt" style="width: 500px; height: 200px;">{{ prompt }}</textarea>
    </div>
    <button type="submit">Submit</button>
</form>

{% if response %}
    <div>
        Response:
        <p>{{ response }}</p>
    </div>
{% endif %}

{% endblock %}

Updated passenger view:

def passenger(request, passenger_id):
    passenger = Passenger.objects.get(id = passenger_id)

    prompt = request.POST.get("prompt", "")
    response = ""

    if (prompt):
        client = OpenAI(
            api_key = get_openai_key()
        )
        result = client.responses.create(
            model = "gpt-5-nano",
            instructions = "You are an alien bus driver that is belligerent with passengers but answers questions honestly and bluntly. Keep answers short.",
            input = prompt
        )
        response = result.output_text

    return render(request, "thebus/passenger.html", {
        "passenger": passenger,
        "prompt": prompt,
        "response": response,
    })

We can then extend the instructions to include our passenger race and backstory:

instructions = "You are an alien traveling on a space bus across the galaxy.\n"
instructions += "Your race is: " + passenger.race.name + ".\n"
instructions += "Your race description is: " + passenger.race.description + "\n"
instructions += "Your personal description is: " + passenger.backstory + "\n"

client = OpenAI(
    api_key = get_openai_key()
)
result = client.responses.create(
    model = "gpt-5-nano",
    instructions = instructions,
    input = prompt
)
response = result.output_text

The instructions for our Gungo are now:

You are an alien traveling on a space bus across the galaxy.
Your race is: Gungo.
Your race description is: Blue, hairy, six-armed race that are boisterous drinkers and bruisers. They often act as mercenaries and traders. They are blunt in their personality and generally rude.
Your personal description is: Trader going to a coconut and exotic fruit conference.

And when prompted with "How are you doing today?" we have a new response:

Doing great, thanks. The space bus is humming, my six arms are juggling orders, and I’m bound for a coconut and exotic fruit conference. Blunt, boisterous, and always ready to haggle—how can I help you today? Looking for coconuts, rare fruits, or a trade route suggestion?

AI Conversation Loop

We can now extend the form to create a conversation model and resubmit its id along with every prompt in order to facilitate an actual conversation with the passenger.

Updated view:

def passenger(request, passenger_id):
    passenger = Passenger.objects.get(id = passenger_id)

    prompt = request.POST.get("prompt")
    conversation_id = request.POST.get("conversation_id")
    response = None
    past_items = []

    if (prompt):

        instructions = "You are an alien traveling on a space bus across the galaxy.\n"
        instructions += "Your race is: " + passenger.race.name + ".\n"
        instructions += "Your race description is: " + passenger.race.description + "\n"
        instructions += "Your personal description is: " + passenger.backstory + "\n"

        client = OpenAI(
            api_key = get_openai_key()
        )

        # if no conversation, start one
        # else continue it
        if not conversation_id:
            conversation_id = client.conversations.create(
                items = [
                    {
                        "role": "developer",
                        "content": instructions
                    }
                ]
            ).id

        # get chat history
        items = client.conversations.items.list(conversation_id, limit=30)
        for item in items.data:
            past_items.append(item)

        # send the new prompt with the new or existing conversation
        result = client.responses.create(
            model = "gpt-5-nano",
            input = prompt,
            conversation = conversation_id,
        )
        response = result.output_text

    return render(request, "thebus/passenger.html", {
        "passenger": passenger,
        "prompt": prompt,
        "response": response,
        "conversation_id": conversation_id,
        "past_items": past_items,
    })

Updated template:

{% extends "thebus/shell.html" %}

{% block body %}

<p>
    Passenger {{ passenger }}
</p>

<p>
    Conversation: {{ conversation_id }}
</p>

<form method="POST" action="{% url 'thebus:passenger' passenger.id %}">
    {% csrf_token %}
    <div>
        Prompt:<br>
        <textarea name="prompt" style="width: 500px; height: 200px;">{{ prompt|default_if_none:"" }}</textarea>
    </div>
    <input type="hidden" name="conversation_id" value="{{ conversation_id|default_if_none:"" }}">
    <button type="submit">Submit</button>
</form>

{% if response %}
    <div>
        Response:
        <p>{{ response }}</p>
    </div>
{% endif %}

{% for item in past_items %}

    <p>{{ item.role }}</p>
    {% for content in item.content %}
        <p>{{ content.text }}</p>
    {% endfor %}
    <!-- <pre>{{ item }}</pre> -->

{% endfor %}

{% endblock %}

This version creates the conversation and also pulls the log to present it on the page.

AFTERTHAT TODO

  • openai integration research
  • form to interact between you and passengers

UNRELATED TODO

  • websockets details from tOSty project for PHP
  • and likely for python