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
制造商零件编号 4111
CABLE A PLUG TO MCR B PLUG 3.28'
Adafruit Industries LLC
制造商零件编号 938
GRAPHIC DISPLAY OLED WHITE 1.3"
Adafruit Industries LLC
制造商零件编号 5200
PICOWBELL PROTO FOR RPI PICO
Adafruit Industries LLC
制造商零件编号 4399
STEMMA QWIIC JST SH CABLE 50MM
Adafruit Industries LLC
制造商零件编号 5583
SOCKET HEADERS FOR RASPBERRY PI
Adafruit Industries LLC
制造商零件编号 3299
BLACK NYLON SCREW AND STAND-OFF
Adafruit Industries LLC
制造商零件编号 3490
SWITCH PUSH SPST-NO BLU 10MA 5V
Adafruit Industries LLC
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