Part 1: How to build an AI-powered bookmarking app with Python and WebAssembly
Tags: tutorial, python, ai, programming
Published: 2024-01-04 16:24:05

Python is a fantastic language for building HTTP apps, and it’s super easy to build such an app using [Spin](https://developer.fermyon.com/spin/v2/index). To kick off the new year, I thought I’d write a series showcasing how to use Python and AI together with Spin. In this series, we’re going to build a Python web app for bookmarking pages. We’ll start simple, building just a basic HTML page and Python form handler. And we’ll work our way all the way to the point of adding an AI-powered summarizer. 

*And we’ll do all of this in 150 lines of code!* 

![The app we will build is a bookmarking tool with AI summarizer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ei98za9ex98et5tvomka.png)


We’ll see how the Spin framework makes it super easy (and super fast) to write this style of app.

Key technologies we’ll work with:

- Serverless functions
- Python 3.11
- The pipenv dependency manager
- The Spin Framework
- Key/Value Storage (a kind of NoSQL database)
- Django2 templates
- The Python http-router
- AI LLM inferencing with Meta’s LLaMa2

Spin is a tool for creating serverless apps. A serverless app is simply an app where you (as the developer) do not need to create the a software server (a daemon) to listen for requests. Instead, you just write request handler functions, and leave it to the environment to handle all the server stuff like starting a socket, listening on a port, managing TLS, and so on.

With that background, let’s dive into part 1. In this part, we will scaffold out a Python app. This will be the foundation for the next part, where we build a bookmarking app. But if you’re just interested in getting a Python serverless function going, this part may be all you need to kickstart your next project.

## Prerequisites

For this project, we’re going to use `spin` with the Python plugin and templates. We’re also going to make use of `pipenv` and `pip`.

You’ll need [Spin 2.0.1 or greater](https://developer.fermyon.com/spin/v2/install). If the `spin plugins list` command does not show you `py2wasm` (the Python to Wasm compiler), you will want to install that and the Python templates:

```console
$ spin plugin install py2wasm
$ spin templates install --git https://github.com/fermyon/spin-python-sdk --update
```

I use [pipenv to manage Python projects](https://pipenv.pypa.io/en/latest/index.html), and will be using it in this project.

## Starting a New Project

With these things installed, it’s quick and easy to get started on our new project. We’ll use the Python `http-py` template to scaffold the project:

```console
$ spin new -t http-py bookmarker
Description: A bookmark app
HTTP path: /...
```

The command above uses the HTTP Python template (`-t http-py`) to create a new app named `bookmarker`. At this point, we can change into the `bookmarker` directory and start coding.

```console
$ cd bookmarker
$ tree .
.
├── Pipfile
├── README.md
├── app.py
└── spin.toml

0 directories, 4 files
```

Here’s a quick overview of the files you can see in the `tree` command above: 

- `Pipfile` is the usual config file that `pipenv` will use to track the project and its dependencies.
- `README.md` is the main README for the project.
- `app.py` is the main code file, and we’ll start working on it in just a moment
- `spin.toml` is the [Spin configuration file](https://developer.fermyon.com/spin/v2/writing-apps)

For the time being, we don’t need to do anything with any of those files. We’re just going to build the scaffolded code and test it out.

```console
$ spin build --up
Building component bookmarker with `spin py2wasm app -o app.wasm`
Spin-compatible module built successfully
Finished building all Spin components
Logging component stdio to ".spin/logs/"

Serving http://127.0.0.1:3000
Available Routes:
  bookmarker: http://127.0.0.1:3000 (wildcard)
```

The `spin build --up` command does two things: 

- It builds our Python app into a WebAssembly binary
- Then it starts a local HTTP server (`--up`) that listens on `http://localhost:3000`

We can use a browser or a command line program like `curl` to check out our app.

```console
$ curl localhost:3000
Hello from the Python SDK
```

And there we have it! A running app! Now let’s turn it into our own thing. You can use `CTRL-C` (or `CMD-C` on macOS) to stop the server.

## Starting Some Code

The code in `app.py` looks like this:

```python
from spin_http import Response
def handle_request(request):
    return Response(
        200,
        {"content-type": "text/plain"},
        bytes(f"Hello from the Python SDK", "utf-8"),
    )
```

The `handle_request()` function is called each time a new inbound HTTP request comes in. It takes one argument, a `Response` object from the `spin_http` package. The `Request` object has a few different properties:

- `uri` is the path that invoked this request
- `method` is the HTTP verb (e.g. `GET`, `POST`, etc) that was sent by the client
- `headers` is a dictionary of HTTP headers, as well as the special [Spin headers](https://developer.fermyon.com/spin/v2/http-trigger#additional-request-information)
- `body` contains the request body (such as `POST` data) if any was sent by the client

In the code above, we sent a `Response` object (also from `spin_http`). Constructing a `Response` takes three bits of information: 

- The HTTP status code (such as `200` for success or `404` for “Not Found”)
- A dictionary of headers, with things like `content-type` to set the content type
- The body of the request, which is sent back to the web browser to render

If we use `curl`’s `-v` flag, we can see all of that on the command line:

```console
$ curl -v localhost:3000/
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< content-length: 25
< date: Wed, 29 Nov 2023 00:02:32 GMT
<
* Connection #0 to host localhost left intact
Hello from the Python SDK
```

Now we can do one quick change to see the results:

```python
from spin_http import Response
def handle_request(request):
    return Response(
        200,
        {"content-type": "text/plain"},
        b"Hello from Bookmarker",
    )
```

All we did here is change the returned body to `b”Hello from Bookmarker”`. The `b` decorator tells Python to return the string as an array of `bytes`, which is shorter than using the `bytes()` function.

If we do `spin build --up` again and test it with `curl`, we see the new result:

```console
$ curl localhost:3000
Hello from Bookmarker
```

Alright, we’ve got the basics down. Let’s get on to something a little more complex: Routing.