Raspberry Pi API Call Series: A Desktop AI App on Raspberry Pi OS

Parts 1 and 2 lived in the terminal. Part 3 gets a proper window — file picker, results display, save button — built with Tkinter, which ships with Python on every Raspberry Pi OS install.
Now It Has a Window: A Desktop AI App on Raspberry Pi OS — Tech-Reader.blog
Tech-Reader.blog  ·  Raspberry Pi AI Series  ·  Part 3 of 4
Tkinter Desktop App  ·  October 2026

Now It Has a Window: A Desktop AI App on Raspberry Pi OS

Tkinter ships with Python. The app runs natively. Nothing extra to install.
Parts 1 and 2 lived in the terminal. Part 3 gets a proper window — file picker, dark-themed results area, save button — built with Tkinter, which ships with Python on every Raspberry Pi OS installation. Zero additional dependencies.

At this point in the series, we have proven the concept twice. A bash script can send a CSV to Groq and get JSON back. A Python script does it more cleanly and saves the output to a file. Both run in a terminal window. Part 3 is where it starts to feel like a real application.

Tkinter is Python's built-in GUI toolkit. It is not the most beautiful framework ever built. But it is capable, it is fast on low-power hardware, and it produces a real native window that looks at home on the Raspberry Pi desktop. No browser. No web server. A window that opens when you run the script and closes when you are done.

How We Built This — With a Little Help

Before writing the Tkinter layout, we pulled up Claude at claude.ai — the free plan — and described what we wanted. Not to write the code for us, but to think through the structure.

Mid-Build Conversation We described the app: a file picker, a text display area, two buttons. We asked Claude whether to use a grid layout or pack layout for Tkinter, and whether to run the API call on a background thread so the UI doesn't freeze while waiting for the response. Claude's answer was direct: use grid for anything with more than two or three elements because it gives you alignment control, and yes — absolutely use threading, because a Tkinter window that freezes for two seconds while an API call completes looks broken even if it isn't. That saved us from finding out the hard way.

This is a workflow worth keeping. Claude on the free web plan is a capable thinking partner when you are mid-build and need to reason through a structural decision. You are not outsourcing the work. You are asking a smart collaborator a specific question and incorporating the answer.

A window that freezes for two seconds while an API call runs looks broken even when it isn't. Threading fixes that before users notice it.

The Complete App

Create a file called ai_csv_app.py. This is the full application:

ai_csv_app.py
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import threading
import requests
import json

GROQ_API_KEY = "your_api_key_here"
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"

class AICSVApp:
    def __init__(self, root):
        self.root = root
        self.root.title("AI CSV Processor")
        self.root.geometry("700x520")
        self.root.configure(bg="#f0f0f0")
        self.csv_path = None
        self.result_json = None
        self.build_ui()

    def build_ui(self):
        title = tk.Label(self.root, text="AI CSV Processor",
            font=("DejaVu Sans", 16, "bold"), bg="#f0f0f0", fg="#1a1a1a")
        title.grid(row=0, column=0, columnspan=3, pady=(20, 4))

        subtitle = tk.Label(self.root, text="Powered by Groq + Llama 3.3",
            font=("DejaVu Sans", 10), bg="#f0f0f0", fg="#666")
        subtitle.grid(row=1, column=0, columnspan=3, pady=(0, 16))

        self.file_label = tk.Label(self.root, text="No file selected",
            font=("DejaVu Sans", 10), bg="#f0f0f0", fg="#444", anchor="w")
        self.file_label.grid(row=2, column=0, padx=(20, 8), sticky="ew")

        pick_btn = tk.Button(self.root, text="Choose CSV",
            command=self.pick_file, bg="#1a5c8a", fg="white",
            font=("DejaVu Sans", 10, "bold"), relief="flat", padx=12, pady=6)
        pick_btn.grid(row=2, column=1, padx=4)

        self.process_btn = tk.Button(self.root, text="Send to AI",
            command=self.process, bg="#2e7d32", fg="white",
            font=("DejaVu Sans", 10, "bold"), relief="flat", padx=12, pady=6,
            state="disabled")
        self.process_btn.grid(row=2, column=2, padx=(4, 20))

        self.status_var = tk.StringVar(value="Ready.")
        status = tk.Label(self.root, textvariable=self.status_var,
            font=("DejaVu Sans", 9), bg="#f0f0f0", fg="#888")
        status.grid(row=3, column=0, columnspan=3, pady=(8, 4))

        self.output = scrolledtext.ScrolledText(self.root,
            font=("DejaVu Mono", 11), bg="#0f1923", fg="#a8d8f0",
            insertbackground="white", relief="flat", padx=12, pady=12, wrap=tk.NONE)
        self.output.grid(row=4, column=0, columnspan=3,
            padx=20, pady=8, sticky="nsew")

        self.save_btn = tk.Button(self.root, text="Save JSON",
            command=self.save_output, bg="#555", fg="white",
            font=("DejaVu Sans", 10), relief="flat", padx=12, pady=6, state="disabled")
        self.save_btn.grid(row=5, column=2, padx=(4, 20), pady=(4, 20))

        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(4, weight=1)

    def pick_file(self):
        path = filedialog.askopenfilename(title="Select a CSV file",
            filetypes=[("CSV files", "*.csv"), ("All files", "*.*")])
        if path:
            self.csv_path = path
            self.file_label.config(text=path.split("/")[-1])
            self.process_btn.config(state="normal")
            self.status_var.set("File loaded. Click 'Send to AI' when ready.")

    def process(self):
        self.process_btn.config(state="disabled")
        self.save_btn.config(state="disabled")
        self.status_var.set("Sending to Groq API...")
        self.output.delete("1.0", tk.END)
        thread = threading.Thread(target=self.run_api_call)
        thread.daemon = True
        thread.start()

    def run_api_call(self):
        try:
            with open(self.csv_path, "r") as f:
                csv_data = f.read()

            prompt = f"""Here is a CSV file with product inventory data:

{csv_data}

Return a JSON array where each item has:
- product (string)
- price (number)
- category (reasonable category based on product name)

Return only valid JSON. No explanation."""

            headers = {"Authorization": f"Bearer {GROQ_API_KEY}",
                       "Content-Type": "application/json"}
            payload = {"model": "llama-3.3-70b-versatile",
                       "messages": [{"role": "user", "content": prompt}],
                       "temperature": 0.2}

            response = requests.post(GROQ_URL, headers=headers, json=payload)
            response.raise_for_status()
            content = response.json()["choices"][0]["message"]["content"]
            self.result_json = json.loads(content)
            pretty = json.dumps(self.result_json, indent=2)
            self.root.after(0, self.show_result, pretty)

        except Exception as e:
            self.root.after(0, self.show_error, str(e))

    def show_result(self, text):
        self.output.insert("1.0", text)
        self.status_var.set("Done. Results displayed below.")
        self.process_btn.config(state="normal")
        self.save_btn.config(state="normal")

    def show_error(self, message):
        self.status_var.set(f"Error: {message}")
        self.process_btn.config(state="normal")
        messagebox.showerror("API Error", message)

    def save_output(self):
        if not self.result_json:
            return
        path = filedialog.asksaveasfilename(defaultextension=".json",
            filetypes=[("JSON files", "*.json")])
        if path:
            with open(path, "w") as f:
                json.dump(self.result_json, f, indent=2)
            self.status_var.set(f"Saved to {path.split('/')[-1]}")

if __name__ == "__main__":
    root = tk.Tk()
    app = AICSVApp(root)
    root.mainloop()

Run it from the terminal:

Terminal
python3 ai_csv_app.py

A window opens. Click Choose CSV, select your inventory file, click Send to AI, and watch the JSON appear in the dark text area. Click Save JSON when you are ready to write it to disk.

0 Additional packages to install — Tkinter ships with Python on Raspberry Pi OS

The Threading Detail

Notice the threading.Thread in the process method. The API call runs on a background thread so the window stays responsive while it waits for Groq. Without that, the window would freeze for the duration of the network call. It would eventually respond. But it would look broken, which is the same problem.

The self.root.after(0, ...) calls bring results back to the main UI thread. Tkinter is not thread-safe, so you never update widgets directly from a background thread. You schedule the update instead. This pattern — background thread for the work, after() for the UI update — is the right structure for any Tkinter app that makes network calls. Use it every time.

· · ·

What we have built across three installments is the same task implemented three different ways — getting progressively more capable with each version. The shell script proved it was possible. The Python CLI made it useful. The desktop app made it accessible to anyone sitting at a keyboard. One more step remains.

Next in the Series  ·  Part 4 of 4
The Full Stack: A Browser-Based AI Dashboard on a Pi 3

Part 4 takes the whole thing into Chromium. A local Flask server, an HTML dashboard, file upload, AI processing, and results rendered as a table in the browser — all running on the Pi. It looks like a web app because it is one.