Maker.io main logo

Generating Text with ChatGPT, Pico W, & CircuitPython

2023-06-20 | By Adafruit Industries

License: See Original Project Displays Programmers Wireless

Courtesy of Adafruit

Guide by Jeff Epler

Overview

 

 

"As an AI language model, I am thrilled to be on the ‎Raspberry Pi Pico W! This small yet powerful device ‎enables seamless integration with a wide range of ‎applications and systems, making it an ideal platform ‎for AI and machine learning projects. The Raspberry ‎Pi Pico W's versatility, simplicity and affordability ‎make it a game-changer in the world of technology!" ‎‎-- ChatGPT

Quote blocks like the one above and photos in this guide typically ‎show text generated by ChatGPT.‎

In this guide, you will learn how to use OpenAI's ChatGPT API to ‎generate text from a prompt using CircuitPython on the Raspberry ‎Pi Pico W.‎

At startup, or when the arcade button is pressed, a new, original ‎snippet of text will be generated on OpenAI's servers and shown on ‎the OLED screen connected to your Pico W. Because of the random ‎factor in the text ChatGPT generates, it's unlikely that two responses ‎would ever be the same.‎

Since ChatGPT generates plausible text rather than making true ‎statements, use this project only for situations where the truth is ‎unimportant. For example, by default the request to ChatGPT asks ‎for a description of an "unconventional but useful superpower".‎

It's easy to customize the prompt using any text editor. This guide ‎has some tips for creating prompts of your own. This works by ‎writing a sentence or two in natural human language describing ‎what you'd like ChatGPT to generate; no complicated coding is ‎needed to get a description of an imaginary plant instead, or even to ‎generate text in French instead of English!‎

The code in this guide does use a paid API at OpenAI, but based on ‎the pricing in March 2023, the cost to access the API for this project is ‎measured in fractions of a cent, not in dollars. During the whole ‎development process of this guide, the author's costs on OpenAI ‎were less than $0.25.‎

Parts

Installing CircuitPython

CircuitPython is a derivative of MicroPython designed to simplify ‎experimentation and education on low-cost microcontrollers. It ‎makes it easier than ever to get prototyping by requiring no upfront ‎desktop software downloads. Simply copy and edit files on ‎the CIRCUITPY drive to iterate.‎

CircuitPython QuickStart

Follow this step-by-step to quickly get CircuitPython working on ‎your board.‎

Download the latest version of CircuitPython for the Raspberry Pi ‎Pico W from circuitpython.org

Click the link above and download the latest UF2 file.‎

Download and save it to your desktop (or wherever is handy).‎

download_1

Start with your Pico W unplugged from USB. Hold down ‎the BOOTSEL button, and while continuing to hold it (don't let go!), ‎plug the Pico W into USB. Continue to hold the BOOTSEL button ‎until the RPI-RP2 drive appears!

If the drive does not appear, unplug your Pico W, and go through the ‎above process again.‎

A lot of people end up using charge-only USB cables and it is very ‎frustrating! So, make sure you have a USB cable you know is good ‎for data sync.‎

button_2

You will see a new disk drive appear called RPI-RP2.‎‎ ‎

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.‎

drive_3

drive_4

The RPI-RP2 drive will disappear, and a new disk drive ‎called CIRCUITPY will appear.

That's it, you're done! :)

new_5

Flash Resetting UF2‎

If your Pico W ever gets into a really weird state and doesn't even ‎show up as a disk drive when installing CircuitPython, try installing ‎this 'nuke' UF2 which will do a 'deep clean' on your Flash Memory. ‎You will lose all the files on the board, but at least you'll be able to ‎revive it! After nuking, re-install CircuitPython.‎

flash_nuke.uf2‎

Create an account with OpenAI

The OpenAI platform is managed by OpenAI and changes at their ‎discretion, and so the details may be slightly different from what is ‎documented here.‎

In your web browser, visit https://platform.openai.com/‎

Click the "sign up" link. Then, you can use your e-mail to sign up, or ‎an existing Google or Microsoft account.‎

OpenAI may require additional steps such as e-mail or phone ‎verification before you can log in to your account.‎

account_6

Once you have completed the verification process and logged in, you ‎will next create an API key. Use the menu in the far upper right ‎corner (probably labeled "Personal") and then select "View API Keys".‎

view_7

Then, create a fresh API key by clicking "Create new secret key".‎

keys_8

Save this secret key in the file settings.toml on the CIRCUITPY drive ‎in a line that looks like

OPENAI_API_KEY="sk-b6...kP5"‎

This file also requires your WiFi credentials, see the next page of the ‎guide for the details.‎

save_9

At the time of writing, OpenAI provides a free credit with new ‎accounts. After the free credit is used or expires, you'll need to enter ‎a credit card in your billing information to keep using the service.‎

Using the project tends to cost a few cents per session at most, and ‎it's easy to limit your monthly bill to a pre-set amount such as $8.00.‎

To set a hard usage limit per month, visit the "Usage Limits" section ‎of the OpenAI website.‎

web_10

web_11

This graph shows the author's usage costs while developing and ‎playtesting an app, a total of $1.27 in API calls.‎

graph_12

Configuring the settings.toml File

This project depends on you adding your WiFi settings and OpenAI ‎API key in order to generate the text adventure.‎

Plug your CircuitPython board into your computer via a known good ‎data + power USB cable. Your board should show up as a thumb ‎drive in your File Explorer / Finder (depending on your operating ‎system) named CIRCUITPY.‎

Create a file with the name settings.toml in the root directory of ‎the CIRCUITPY drive.‎

Edit it to contain the keys WIFI_SSID, WIFI_PASSWORD, and OPENAI_API_KEY. ‎‎(It's also OK for it to contain other keys)‎

Your file should look similar to the one shown below:‎

Download File

Copy Code
OPENAI_API_KEY="sk-b6...kP5"
WIFI_SSID="GuestAP"
WIFI_PASSWORD="i trust u"

‎3D Printing‎

It's like watching a delicious pizza come out of the ‎oven, but instead of pizza, it's a perfectly printed part. ‎‎– ChatGPT

This project uses a variant of the case from the guide Pico W HTTP ‎Server with CircuitPython. The case body was remixed using the free ‎and open source OpenSCAD (runs on Windows, Mac, & Linux) to ‎make it deeper and add a hole for the arcade button. Use the face ‎plate from the original project but grab the printable STL for the new ‎case body using the link below or grab the OpenSCAD source if you ‎want to customize it further. Follow the printing instructions from ‎the HTTP Server guide.‎

enlarged_picow.stl (printable case body)‎‎

Download File

Copy Code
module base() { import("picowServerCase_mainCase_v1.stl", convexity=4); }
module clip_removed() {
    difference() {
        base();

        translate([-69,11,-40])
        cube(25);
    }
}

module z_slice(z0, dz, sc, extent=300) {
    render(convexity=4) intersection() {
        translate([0,0,-z0])
        children();

        translate([-extent/2, -extent/2, 0])
        cube([extent, extent, dz]);
    }
}

module elongated() {
    color("red")
    z_slice(-32, 4)
    clip_removed();

    color("green")
    translate([0,0,4])
    scale([1,1,8])
    z_slice(-28, 1)
    clip_removed();

    translate([0,0,12])
    z_slice(-27, 30)
    clip_removed();
}

module arcaded() {
    difference() {
        elongated();

        translate([-45, 53/2, 18])
        rotate([0,90,0])
        cylinder(d=29.5, h=10);
    }
}

arcaded();

Assembly

As an AI language model, I don't have personal ‎experiences to share, but I can generate a sentence ‎for you: "Once, I accidentally soldered a piece of ‎spaghetti onto my circuit board, and it surprisingly ‎still worked." – ChatGPT

Some assembly photos are from the guide "Pico W HTTP Server with ‎CircuitPython", so the parts shown don't match the rest of the ‎project.‎

Solder socket headers to the PiCowbell. You can use the Pico W as a ‎jig to keep the headers secure.‎

headers_13

headers_14

Attach four M2 standoffs to the Pico W's four mounting holes with ‎M2 nuts.‎

attach_15

Attach the Pico W to the case lid with four M2 screws.‎

case_16

case_17

Attach the OLED screen with four M2.5 screws and nuts.‎

plug_17a

plug_17b

Plug the PiCowbell into the Pico W. The STEMMA port on the ‎PiCowbell should be below the USB port on the Pico W.‎

plug_18

Connect the OLED to the STEMMA port on the PiCowbell with a ‎STEMMA QT cable.‎

connect_19

Solder two 2-pin sections of 90-degree header:‎

  • Pin 14 and the GND pin next to it

  • Pin 10 and the GND pin next to it

Point the long side of the header outwards.‎

point_20

Take two of the quick-connect wire harnesses. Press the ends gently ‎but firmly onto the arcade button. Then pass the wire harnesses ‎through the hole in the case, through the threaded nut, and attach ‎them to the right-angle headers.‎

Plug the button in on Pin 14 and the LED on Pin 10. Check the ‎polarity of the LED, so that side marked "-" on the Arcade Button is ‎connected to GND.‎

At this point, you can load the code and make sure that the button ‎and LED work. If the LED is not working, try reversing the connection ‎by simply plugging the connector in the other way. If nothing's ‎working, try swapping the two connectors.‎

load_21

Now, secure the arcade button in place with the nut, then carefully ‎curl the wires around the interior of the box until you can snap the ‎lid into place.

button_22

Coding the Text Generator

Learning to program is like having a superpower that ‎enables you to create something out of nothing with ‎just a few lines of code. – ChatGP

Text Editor

Adafruit recommends using the Mu editor for editing your ‎CircuitPython code. You can get more info in this guide.‎

Alternatively, you can use any text editor that saves simple text files.‎

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and ‎the code.py file. To get everything you need, click on the Download ‎Project Bundle link below, and uncompress the .zip file.‎

Hook your Pico W to your computer via a known good USB ‎data+power cable. It should show up as a thumb drive ‎named CIRCUITPY.‎

Using File Explorer/Finder (depending on your Operating System), ‎drag the contents of the uncompressed bundle directory onto your ‎board's CIRCUITPY drive, replacing any existing files or directories ‎with the same names, and adding any new ones that are necessary.‎

Once the code restarts, it will connect to WiFi and start using OpenAI ‎to generate text according to the default prompt, "Write 1 sentence ‎starting "you can" about an unconventional but useful superpower". ‎The code uses OpenAI's "streaming" mode, so the response appears ‎by chunks, also known as tokens.‎

Head on to the next pages for advice on how to modify it with your ‎own original prompts as well as explanation of key parts of the code.‎

head_23

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: MIT
import json
import os
import ssl
import traceback

import board
import displayio
import digitalio
import keypad
import socketpool
import supervisor
from wifi import radio

import adafruit_requests
import adafruit_displayio_ssd1306
from adafruit_bitmap_font.bitmap_font import load_font
from adafruit_display_text import wrap_text_to_pixels
from adafruit_display_text.bitmap_label import Label
from adafruit_ticks import ticks_add, ticks_less, ticks_ms


# Choose your own prompt and wait messages, either by changing it below inside
# the """triple quoted""" string, or by putting it in your settings.toml file,
# like so:
#
# MY_PROMPT="Give me an idea for a gluten free, keto dinner. Write one sentence"
# PLEASE_WAIT="Cooking something up just for you"
#
# Experimentation is best to figure out what works. Usually you'll want to ask
# for just one sentence or paragraph, since the 128x32 pixel screen can't hold
# much text!

# Here are some that the author found worked reasonably well:

# Give me an idea for a plant-based dinner. Write one sentence
#
# Give jepler (they/them) a cliched and flowery description as a comic book
# supervillain. write one sentence.
#
# Invent and describe an alien species. write one sentence
#
# Invent a zany 'as seen on' product that can't possibly work. One sentence
#
# Tell a 1-sentence story about a kitten and a funny mishap
#
# Make up a 1-sentence fortune for me
#
# In first person, write a 1-sentence story about an AI avoiding boredom in a creative way.
#
# Pick an everyday object (don't say what it is) and describe it using only the
# ten hundred most common words.
#
# Invent an alien animal or plant, name it, and vividly describe it in 1
# sentence
#
# Invent and vividly describe an alien species. write one paragraph

prompt=os.getenv("MY_PROMPT", """
Write 1 sentence starting "you can" about an unconventional but useful superpower
""").strip()
please_wait=os.getenv("PLEASE_WAIT", """
Finding superpower
""").strip()

openai_api_key = os.getenv("OPENAI_API_KEY")

nice_font = load_font("helvR08.pcf")
line_spacing = 9 # in pixels

#  i2c display setup
displayio.release_displays()
oled_reset = board.GP9

# STEMMA I2C on picowbell
i2c = board.STEMMA_I2C()
display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset)

WIDTH = 128
HEIGHT = 64

display = adafruit_displayio_ssd1306.SSD1306(
    display_bus, width=WIDTH, height=HEIGHT
)
if openai_api_key is None:
    input("Place your\nOPENAI_API_KEY\nin settings.toml")
display.auto_refresh = False

class WrappedTextDisplay(displayio.Group):
    def __init__(self):
        super().__init__()
        self.offset = 0
        self.max_lines = display.height // line_spacing
        for i in range(self.max_lines):
            self.make_label("", i * line_spacing)
        self.lines = [""]
        self.text = ""

    def make_label(self, text, y):
        result = Label(
            font=nice_font,
            color=0xFFFFFF,
            background_color=0,
            line_spacing=line_spacing,
            anchor_point=(0, 0),
            anchored_position=(0, y),
            text=text)
        self.append(result)

    def add_text(self, new_text):
        print(end=new_text)
        if self.lines:
            text = self.lines[-1] + new_text
        else:
            text = new_text
        self.lines[-1:] = wrap_text_to_pixels(text, display.width, nice_font)
        self.scroll_to_end()

    def set_text(self, text):
        print("\n\n", end=text)
        self.text = text
        self.lines = wrap_text_to_pixels(text, display.width, nice_font)
        self.offset = 0

    def show(self, text):
        self.set_text(text)
        self.refresh()

    def add_show(self, new_text):
        self.add_text(new_text)
        self.refresh()

    def scroll_to_end(self):
        self.offset = self.max_offset()

    def scroll_next_line(self):
        max_offset = self.max_offset()
        self.offset = (self.offset + 1) % (max_offset + 1)

    def max_offset(self):
        return max(0, len(self.lines) - self.max_lines)

    def on_last_line(self):
        return self.offset == self.max_offset()

    def refresh(self):
        lines = self.lines
        # update labels from wrapped text, accounting for scroll offset
        for i in range(len(self)):
            offset_i = i + self.offset
            if offset_i >= len(lines):
                text = ""
            else:
                text = lines[offset_i]
            if text != self[i].text:
                self[i].text = text

        # Actually update the display all at once
        display.refresh()

display.root_group = wrapped_text = WrappedTextDisplay()

def wait_button_scroll_text():
    led.switch_to_output(True)
    keys.events.clear()
    deadline = ticks_add(ticks_ms(),
            5000 if wrapped_text.on_last_line() else 1000)
    while True:
        if (event := keys.events.get()) and event.pressed:
            break
        if wrapped_text.max_offset() > 0 and ticks_less(deadline, ticks_ms()):
            wrapped_text.scroll_next_line()
            wrapped_text.refresh()
            deadline = ticks_add(deadline,
                    5000 if wrapped_text.on_last_line() else 1000)
    led.value = False

if radio.ipv4_address is None:
    wrapped_text.show(f"connecting to {os.getenv('WIFI_SSID')}")
    radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD'))
requests = adafruit_requests.Session(socketpool.SocketPool(radio), ssl.create_default_context())

def iter_lines(resp):
    partial_line = []
    for c in resp.iter_content():
        if c == b'\n':
            yield (b"".join(partial_line)).decode('utf-8')
            del partial_line[:]
        else:
            partial_line.append(c)
    if partial_line:
        yield (b"".join(partial_line)).decode('utf-8')

full_prompt = [
    {"role": "user", "content": prompt},
]

keys = keypad.Keys((board.GP14,), value_when_pressed=False)
led = digitalio.DigitalInOut(board.GP10)
led.switch_to_output(False)

try:
    while True:
        wrapped_text.show(please_wait)

        with requests.post("https://api.openai.com/v1/chat/completions",
            json={"model": "gpt-3.5-turbo", "messages": full_prompt, "stream": True},
            headers={
                "Authorization": f"Bearer {openai_api_key}",
            },
            ) as response:

            wrapped_text.set_text("")
            if response.status_code != 200:
                wrapped_text.show(f"Uh oh! {response.status_code}: {response.reason}")
            else:
                wrapped_text.show("")
                for line in iter_lines(response):
                    led.switch_to_output(True)
                    if line.startswith("data: [DONE]"):
                        break
                    if line.startswith("data:"):
                        content = json.loads(line[5:])
                        try:
                            token = content['choices'][0]['delta'].get('content', '')
                        except (KeyError, IndexError) as e:
                            token = None
                        led.value = False
                        if token:
                            wrapped_text.add_show(token)
                wait_button_scroll_text()
except Exception as e: # pylint: disable=broad-except
    traceback.print_exception(e) # pylint: disable=no-value-for-parameter
    print(end="\n\n\nAn error occurred\n\nPress button\nto reload")
    display.root_group = displayio.CIRCUITPYTHON_TERMINAL
    display.auto_refresh = True
    while True:
        if (event1 := keys.events.get()) and event1.pressed:
            break
    supervisor.reload()

View on GitHub

Customizing the Text Generator

Creating a chatbot prompt is like crafting a key that ‎unlocks a door to productive conversation with your ‎audience. -- ChatGPT

With this project, it's easy to customize the "prompt", meaning the ‎text that is sent to ChatGPT and used to generate the response. You ‎can do this by adding or editing two lines in the settings.toml file on ‎your CIRCUITPY drive. This isn't coding per se (because you write in ‎plain english), though there are a few simple rules you have to follow:‎

  • You can't embed newlines or blank lines, so keep each item on ‎its own single line

  • Double quote characters (") have special meaning, so if you ‎want to put quotes inside your prompt or the "please wait" text, ‎the simplest solution is to use single quotes (') instead

  • If your editor replaces double quote characters ("...") with smart ‎quote characters (“…”) it won't work, so disable this or use a ‎simple text editor that doesn't do it.‎

Add your lines to the settings.toml but don't remove the lines for ‎WiFi and API keys you made earlier!

‎For example, here are the settings.toml lines for a 'magical ‎university' prompt, together with other settings required for the ‎project:‎

Download File

Copy Code
PLEASE_WAIT="Somewhere in the halls of Jynx University..."
MY_PROMPT="Write a vivid description of a magical mishap at the Jynx University for Witches and Warlocks (1 sentence; no frogs; no injuries; don't say the school's name)"
OPENAI_API_KEY="sk-b6...kP5"
WIFI_SSID="GuestAP"
WIFI_PASSWORD="i trust u"

A few seconds after saving the file, CircuitPython will re-load and ‎show text based on the new prompt.‎

It takes trial and error to transform a mediocre prompt into a good ‎one. So, feel free to try slight re-wordings of your prompt to see what ‎variations you like best! Here are some of the things I try to do when ‎composing one:‎

  • It seems silly but asking for a "vivid description" actually does ‎tend to make ChatGPT produce more descriptive language

  • Ask for a short length that's more likely to fit on the screen ("1 ‎sentence")‎

  • You can tell it things you don't want ("no frogs; no injuries")‎

  • You can ask it to return the answer in a certain format

  • If the prompt doesn't end with a sentence-ending period ‎sometimes ChatGPT's response starts with just a "." followed by ‎a blank line, so always include a "."‎

Of course, these things are only guidelines and the text returned by ‎ChatGPT may not precisely match what you've asked for.

‎If you want to keep a prompt around for later, you can put a ‎comment character (#) in front of each line, then write your new ‎prompt above and below it. Then, to switch prompts, simply put # in ‎front of the current prompt and remove them from the next prompt ‎you want to use. If all the lines are marked with # at the beginning of ‎the line, then the prompt that is built into the program will be used. ‎Here are some other prompts I tested, but commented out:‎

Download File

Copy Code
#PLEASE_WAIT="pip install --random"
#MY_PROMPT="Make up a humorous, obviously fictional module on pypi (give it a name). Use the following format:\npip install <modulename>\n\n<1 sentence blurb>"

#PLEASE_WAIT="This Person Does Not Exist"
#MY_PROMPT="Invent a character and describe them vividly in 1 sentence"

You can write prompts in other languages as well (the font included ‎in this project supports many European languages):‎

Download File 

Copy Code
PLEASE_WAIT="Trouver votre superpuissance"
MY_PROMPT="Ecrivez 1 phrase commençant par 'vous pouvez' à propos d'une superpuissance non conventionnelle mais utile."

While using Large Language Models like ChatGPT for "factual ‎things" is not the best idea, and this guide has concentrated on ‎fictional items, you may also find that there are practical things you ‎could do with it, such as get meal ideas:‎

Download File

Copy Code
MY_PROMPT="Give me an idea for a gluten free, keto dinner. Write one sentence"
PLEASE_WAIT="Cooking something up just for you"

Here are some more prompts the author thought were amusing or ‎useful:‎

  • Invent a zany 'as seen on' product that can't possibly work. One ‎sentence

  • Tell a 1-sentence story about a kitten and a funny mishap

  • Make up a 1-sentence fortune for me

  • In first person, write a 1-sentence story about an AI avoiding ‎boredom in a creative way

  • Pick an everyday object (don't say what it is) and describe it ‎using only the ten hundred most common words

  • Invent an alien animal or plant, name it, and vividly describe it ‎in 1 sentence

  • Invent and vividly describe an alien species. write one ‎paragraph

  • What's one possible synergy between CircuitPython & ChatGPT? ‎‎(1 sentence, practical)‎

Code Walkthrough

CircuitPython could potentially be used to create a ‎chatbot that can interact with users through ‎hardware devices like sensors and LEDs -- ChatGPT

This program is large and complex. This guide will gloss over a lot of ‎the details; if you'd like to learn more about using ‎WiFi or displayio on CircuitPython there are dedicated guides for ‎those topics. Below you'll find explanations of some key parts of the ‎program functionality.‎

Fetching optional items from settings.toml

os.getenv can be used to fetch string values from ‎the settings.toml file. Providing a second argument gives a default ‎value when the key is not present. Calling strip() removes any ‎whitespace from the start or end of the string, which can trip up ‎ChatGPT. This is an easy way to add "no-code" customizations to ‎your own CircuitPython programs:‎

Download File

Copy Code
prompt=os.getenv("MY_PROMPT", """
Write 1 sentence starting "you can" about an unconventional but useful superpower
""").strip()
please_wait=os.getenv("PLEASE_WAIT", """
Finding superpower
""").strip()

Vertically scrolling wrapped text

There aren't yet any CircuitPython libraries for dealing with larger ‎amounts of text. This project includes a class ‎called WrappedTextDisplay which can help.‎

It allows showing a screen full of text which can be part of a larger ‎document. The text can be scrolled by lines, and new words can be ‎added at the end of the text incrementally, with relatively good ‎performance.‎

This functionality is very closely matched to the needs of this ‎program, but it could provide some ideas for a future library.‎

The text is parsed into lines as it arrives. A certain number of lines ‎can be visible on the screen at one time, an each one of those visible ‎lines of text gets its own bitmap label object. Scrolling consists of ‎changing which logical line in the document is the first line visible ‎on the display. This approach performs relatively well in terms of ‎both repaint time—especially while streaming content from ‎ChatGPT—and memory used.‎

Download File

Copy Code
class WrappedTextDisplay(displayio.Group):
	...

Once a response from ChatGPT is complete, this function repeatedly ‎scrolls through the full response if it's more than one screenful, then ‎returns when the button is pressed:‎

Download File

Copy Code
def wait_button_scroll_text():
    led.switch_to_output(True)
    deadline = ticks_add(ticks_ms(),
            5000 if wrapped_text.on_last_line() else 1000)
    while True:
        if (event := keys.events.get()) and event.pressed:
            break
        if wrapped_text.max_offset() > 0 and ticks_less(deadline, ticks_ms()):
            wrapped_text.scroll_next_line()
            wrapped_text.refresh()
            deadline = ticks_add(deadline,
                    5000 if wrapped_text.on_last_line() else 1000)
    led.value = False

 

Streaming an HTTP response by lines

When the OpenAI API is used with "stream": True, the text is ‎returned as it is generated. You can check the OpenAI API ‎documentation for more details, but in short, each line starts "data:" ‎followed by a JSON document all on one line.‎

By using the iter_lines function you can handle the HTTP response ‎one line at a time:‎

Download File

Copy Code
def iter_lines(resp):
    partial_line = []
    for c in resp.iter_content():
        if c == b'\n':
            yield (b"".join(partial_line)).decode('utf-8')
            del partial_line[:]
        else:
            partial_line.append(c)
    if partial_line:
        yield (b"".join(partial_line)).decode('utf-8')

Prompt and Request

In this program, the Prompt is simple, it consists of just a single ‎‎"user" message. The content of the message is the prompt, either ‎from the top of the code or from settings.toml:‎

Download File

Copy Code
full_prompt = [
    {"role": "user", "content": prompt},
]

When there is a dialogue between ChatGPT and the user that takes ‎place over multiple exchanges, then the full_prompt can consist of ‎multiple messages with various roles (system, user, and assistant). ‎But in this project, there's just one prompt and one response.‎

Within the program's forever loop, the full prompt is put together ‎with other information in the json payload of the request ‎‎(See OpenAI's dedicated documentation pages for more details on ‎the API):‎

Download File

Copy Code
while True:
        wrapped_text.show(please_wait)

        with requests.post("https://api.openai.com/v1/chat/completions",
            json={"model": "gpt-3.5-turbo", "messages": full_prompt, "stream": True},
            headers={
                "Authorization": f"Bearer {openai_api_key}",
            },
            ) as response:

If the response is successful, then it can be read line by line; each line ‎may contain some additional part of the response (called the "delta"). ‎This text is added to the display, which is refreshed. Just to ‎emphasize that something is happening, the LED blinks during this ‎process. At the end, the program waits for the button to be pressed ‎before it repeats the process again.‎

Download File

Copy Code
#
            if response.status_code != 200:
                wrapped_text.show(f"Uh oh! {response.status_code}: {response.reason}")
            else:
                wrapped_text.show("")
                for line in iter_lines(response):
                    led.switch_to_output(True)
                    if line.startswith("data: [DONE]"):
                        break
                    if line.startswith("data:"):
                        content = json.loads(line[5:])
                        try:
                            token = content['choices'][0]['delta'].get('content', '')
                        except (KeyError, IndexError) as e:
                            token = None
                        led.value = False
                        if token:
                            wrapped_text.add_show(token)
                wait_button_scroll_text()

Error handling

In the case of most errors, the program catches the error so that a ‎press of the button can re-start from scratch. The original error is ‎shown on the REPL for troubleshooting purposes.‎

Download File

Copy Code
except Exception as e: # pylint: disable=broad-except
    traceback.print_exception(e) # pylint: disable=no-value-for-parameter
    print(end="\n\n\nAn error occurred\n\nPress button\nto reload")
    display.root_group = displayio.CIRCUITPYTHON_TERMINAL
    display.auto_refresh = True
    while True:
        if (event1 := keys.events.get()) and event1.pressed:
            break
    supervisor.reload()
制造商零件编号 SC0918
RASPBERRY PI PICO W RP2040
Raspberry Pi
¥48.84
Details
制造商零件编号 4111
CABLE A PLUG TO MCR B PLUG 3.28'
Adafruit Industries LLC
¥32.15
Details
制造商零件编号 938
GRAPHIC DISPLAY OLED WHITE 1.3"
Adafruit Industries LLC
¥168.09
Details
制造商零件编号 5200
PICOWBELL PROTO FOR RPI PICO
Adafruit Industries LLC
¥15.87
Details
制造商零件编号 4399
STEMMA QWIIC JST SH CABLE 50MM
Adafruit Industries LLC
¥7.73
Details
制造商零件编号 5583
SOCKET HEADERS FOR RASPBERRY PI
Adafruit Industries LLC
¥7.73
Details
制造商零件编号 3299
BLACK NYLON SCREW AND STAND-OFF
Adafruit Industries LLC
¥145.84
Details
制造商零件编号 3490
SWITCH PUSH SPST-NO BLU 10MA 5V
Adafruit Industries LLC
¥20.35
Details
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum