We are building a lightweight desktop writing assistant that rewrites and summarizes text using an LLM backend. This is the kind of tool you keep in the background while drafting emails or documentation. I will walk through a complete tkinter application that streams responses from Oxlo.ai using the OpenAI-compatible SDK.
What you'll need
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI Python SDK:
pip install openai
Step 1: Create the UI shell
I start with a single-file tkinter app. The layout has an input box, an output box, and a row of action buttons.
import tkinter as tk
from tkinter import ttk
class WriterAssist:
def __init__(self, root):
self.root = root
self.root.title("Writer Assist")
self.root.geometry("800x600")
self.input_box = tk.Text(root, wrap=tk.WORD, height=10)
self.input_box.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
btn_frame = ttk.Frame(root)
btn_frame.pack(fill=tk.X, padx=8, pady=4)
self.rewrite_btn = ttk.Button(btn_frame, text="Rewrite", command=self.on_rewrite)
self.rewrite_btn.pack(side=tk.LEFT, padx=2)
self.output_box = tk.Text(root, wrap=tk.WORD, height=10, state=tk.DISABLED)
self.output_box.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
def on_rewrite(self):
pass
if __name__ == "__main__":
app = tk.Tk()
WriterAssist(app)
app.mainloop()
Step 2: Configure the Oxlo.ai client
We initialize the OpenAI SDK pointing at Oxlo.ai. I also define the system prompt in its own constant so the model knows it is an editor, not a chatbot.
SYSTEM_PROMPT = """You are a concise writing assistant. Given a user's text, rewrite it to improve clarity and flow. Preserve the original meaning and tone. Do not add greetings or explanations. Output only the rewritten text."""
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
Step 3: Add the rewrite handler
I fetch the input text, send it to Llama 3.3 70B on Oxlo.ai, and write the result into the output box.
def on_rewrite(self):
user_message = self.input_box.get("1.0", tk.END).strip()
if not user_message:
return
self.output_box.configure(state=tk.NORMAL)
self.output_box.delete("1.0", tk.END)
self.output_box.configure(state=tk.DISABLED)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
)
result = response.choices[0].message.content
self.output_box.configure(state=tk.NORMAL)
self.output_box.insert(tk.END, result)
self.output_box.configure(state=tk.DISABLED)
Step 4: Stream tokens into the output box
Blocking calls freeze the UI. Switching to streaming lets us write tokens as they arrive so the interface stays responsive.
def on_rewrite(self):
user_message = self.input_box.get("1.0", tk.END).strip()
if not user_message:
return
self.output_box.configure(state=tk.NORMAL)
self.output_box.delete("1.0", tk.END)
stream = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
stream=True,
)
for chunk in stream:
token = chunk.choices[0].delta.content or ""
self.output_box.insert(tk.END, token)
self.output_box.see(tk.END)
self.root.update_idletasks()
self.output_box.configure(state=tk.DISABLED)
Step 5: Add error handling and a second action
Network calls fail, so I wrap the stream in a try block. I also add a Summarize button that reuses the same client but changes the user instruction.
def call_oxlo(self, instruction, user_text):
self.output_box.configure(state=tk.NORMAL)
self.output_box.delete("1.0", tk.END)
try:
stream = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"{instruction}: {user_text}"},
],
stream=True,
)
for chunk in stream:
token = chunk.choices[0].delta.content or ""
self.output_box.insert(tk.END, token)
self.output_box.see(tk.END)
self.root.update_idletasks()
except Exception as e:
self.output_box.insert(tk.END, f"\nError: {e}")
self.output_box.configure(state=tk.DISABLED)
def on_rewrite(self):
text = self.input_box.get("1.0", tk.END).strip()
if text:
self.call_oxlo("Rewrite the following text", text)
def on_summarize(self):
text = self.input_box.get("1.0", tk.END).strip()
if text:
self.call_oxlo("Summarize the following text in one paragraph", text)
Wire the second button inside __init__:
self.summarize_btn = ttk.Button(btn_frame, text="Summarize", command=self.on_summarize)
self.summarize_btn.pack(side=tk.LEFT, padx=2)
Run it
Save everything as writer_assist.py, set your key, and launch.
export OXLO_API_KEY="sk-oxlo.ai-..."
python writer_assist.py
Paste in rough text like "i need to email the team about the deploy but it is broke and i dont know what to say", then click Rewrite. After a few seconds the output box fills with clean text. Example output:
I need to email the team about the deployment, but it is broken and I am unsure what to say.
Click Summarize and it collapses the input to a single concise statement.
Next steps
Add a global keyboard shortcut so the app activates from the system tray, or switch to qwen-3-32b when you need multilingual rewriting. If you roll this out to a team, Oxlo.ai request-based pricing keeps costs flat even when users paste long documents, which is worth testing against your current token-based provider.
Top comments (0)