[ Back to home ]
2024
A dual-interface personal AI assistant.
Press Ctrl+Alt+Z for an instant popup. Launch the Electron dashboard for the full workspace. Both surfaces talk to one Flask API and one SQLite memory store, so the assistant feels like one tool no matter where it is invoked from.
1. Abstract
Personal AI assistants force a tradeoff. A popup is fast but shallow. A desktop app does everything but opens too slowly for the small task that prompted the question. Zephyr ships both over one backend, so memory and integrations stay consistent. Quick command, popup. Complex management, dashboard. The assistant never has two minds.
A Tkinter popup on a system-wide hotkey (Ctrl+Alt+Z) handles instant capture. A Flask API on localhost owns all state. An Electron + React dashboard holds the full workspace. SQLite holds persistent memory, and a multi-LLM router fronts Groq Llama 3.1 8B, OpenAI, and Gemini so one provider outage does not take the assistant down.
The novelty is the surface design, not the LLM. Multi-LLM routing, SQLite-backed memory and system-tray apps all exist. The contribution is the discipline that makes two interfaces over one backend feel like one product.
- Three-tier architecture. Tkinter popup, Flask API on
localhost:5000, Electron + React dashboard. Zero direct UI-to-UI traffic.
- Sub-100 ms memory lookups from SQLite across thousands of stored facts, scored to favour recent and frequently accessed entries.
- Multi-LLM router with provider fallback. Groq Llama 3.1 8B by default for speed; OpenAI and Gemini take over automatically when it is unavailable.
- Streaming responses to both surfaces through the same Flask endpoint.
- Plugin-style integrations. Spotify, Google Calendar, WeatherAPI and local file search, each a Python module with a
can_handle() and an execute(). No core changes, no router edits.
2. Introduction
Phone voice assistants are fast but shallow; desktop AI apps are deep but slow to launch. Real tasks span one memory but want different interfaces.
- Quick tasks need a fast capture surface. "Remind me to email the registrar tomorrow." Opening a dashboard kills the impulse before the thought reaches the keyboard.
- Complex tasks need a workspace. "Pull up everything we logged about the rehab study, group by week, export to markdown." That is dashboard work, not popup work.
- Both share memory. What you tell the popup must reach the dashboard, and vice versa, without manual sync.
2.1 Why three tiers, not two
One process shipping both UIs breaks once either UI must update independently, run on another machine, or make room for a third client (a mobile companion, a Slack bot, a voice device). Three tiers make the API the only stable contract. Cost: an extra process. Benefit: the assistant grows without rebuilding state management.
- The API is the abstraction boundary. All state lives in the Flask server. The popup is a UI, not a backend with a UI on top.
- The UIs share only the API contract. Tkinter and Electron speak the same JSON over HTTP, and nothing else.
2.2 What is actually new
Tkinter, Flask, Electron and the LLM providers are all off the shelf. The structure that makes them one product is new.
- Two interfaces, one backend, zero state drift. Popup memory writes reach the dashboard within one API roundtrip. There is no sync layer because there is no second source of truth.
- Provider-agnostic LLM layer. Groq, OpenAI, and Gemini are interchangeable for the same task. Calling code never needs to know a fallback happened.
- Plugin-shaped integrations. Every external service is a two-function handler. Adding one is a single file plus a single import.
Everything below makes those three commitments hold across two UI surfaces.
3. System
3.1 Tiny Zephyr (the popup)
- Python + Tkinter with a dark theme, slide-in animation, and centring on the active monitor.
- Global hotkey (
Ctrl+Alt+Z) registered system-wide on Windows. Brings the window to focus from any state.
- Auto-hide on focus loss, plus a system-tray presence that keeps the hotkey reachable across sessions.
- Voice input feeds the same NLP pipeline as typed input. Nothing branches on modality past capture.
- Streaming responses with concurrent text-to-speech. The assistant talks while the text materialises, so perceived latency stays under the LLM's full generation time.
3.2 Flask API bridge
Runs on localhost:5000, auto-started with Tiny Zephyr but addressable independently.
POST /command is the natural-language entry point. Routes to the right handler, executes, and returns the answer plus any side effects (memory writes, integration calls).
GET / POST / DELETE /memory provides full CRUD over the SQLite memory store, identical for both UIs.
GET / POST /projects tracks projects and ideas with tags and status. The dashboard's Kanban board reads it; the popup writes by phrase ("add project: rewrite the portfolio").
- Per-integration endpoints for Spotify, Weather, and Calendar, each a thin wrapper over the upstream API plus response shaping.
- WebSocket channel pushes popup activity to the dashboard live, so a captured fact appears in the memory browser without a refresh.
3.3 Electron dashboard
- React 18 + Vite + TailwindCSS + Framer Motion, wrapped in Electron for tray, autostart, and OS notifications.
- Conversation history with threaded view, search, filter, and JSON or Markdown export. Popup conversations render here without translation.
- Memory browser with categorised facts, a tag cloud, manual edits, and bulk cleanup of stale entries.
- Project board: Kanban with drag-and-drop reordering, Markdown descriptions, and attachments.
- Settings panel for LLM API keys, integration credentials, theme, and hotkey behaviour, all written through the API rather than to local files.
3.4 Memory and routing
- SQLite-backed memory holds learned facts, preferences, conversation context, and project ideas in one store.
- Relevance scoring favours recent and frequently accessed memories, so a months-old fact does not crowd out something mentioned this morning.
- Command router escalates in three tiers: exact match on registered intents, fuzzy match on phrasing, then LLM intent classification. The cheap tiers absorb most traffic.
3.5 Integrations
- Spotify via Spotipy. Play, pause, skip, "what is playing," and queue from natural language.
- WeatherAPI. System-location or explicit-city queries with contextual advice ("bring a jacket").
- Google Calendar. Event listing and creation with natural-language dates ("add a study block tomorrow at 7 pm").
- Local file search. "Find my resume," "open project folder" against configured paths, so the assistant acts on local state too.
4. Evaluation
Evaluation is functional, measured on the running daily-use install rather than against a benchmark suite.
- Memory lookups under 100 ms across thousands of stored facts, timed directly on the API endpoint.
- Fallback survives provider outages. When Groq has been unavailable, the router has rolled over to OpenAI without user-visible interruption.
- Streaming responses materialise before the LLM finishes generating. The perceived latency floor is the network roundtrip to the provider, not the architecture.
- Modular handlers. Adding Spotify, Calendar, and Weather required no core router changes.
- Two surfaces, one memory. A fact captured through the popup appears in the dashboard's memory browser by the next render, observed during use.
These measure this build on its own protocol, not against another assistant. Personal assistants have no settled benchmark suite, and Zephyr's two-surface design is not directly comparable to single-surface OS-native assistants. Read them as evidence of a working daily tool, not a leaderboard claim.
5. Conclusion and Implications
The right interface depends on the task, not the assistant. Splitting popup and dashboard while unifying state behind an API lets the user pick the mode without losing coherence. Any future surface (mobile companion, voice device, Slack bot, terminal client) becomes another client, not a fork. Local LLMs (Ollama, llama.cpp, LM Studio) slot in without redesign. For a personal tool that must live in the daily workflow without becoming another app to manage, that boundary is the load-bearing one.
5.1 Limitations and future work
- Windows-only. Hotkey, tray, and notifications use Windows-specific APIs. macOS and Linux ports need
pynput for hotkeys and AppKit and GTK shims over the identical Flask backend.
- Single instance only. Two popups conflict on the global hotkey and on port 5000. There is no multi-user mode.
- Cloud LLMs power the deep features. Offline mode covers file operations and cached memory. Ollama or llama.cpp support would make it fully offline as a new backend, not a redesign.
- Plaintext memory at rest. Acceptable for a single-user local tool, not for multi-device sync. Next: optional encrypted cloud sync, with a React Native mobile companion as the first additional surface.
- Integrations cover the author's stack. Others write their own handler today; a plugin marketplace with sandboxed third-party handlers would end forking the core.
6. Acknowledgments
Built independently. LLM inference relies on Groq, OpenAI, and Google Gemini. Spotipy wraps the Spotify Web API; WeatherAPI and the Google Calendar API supply the remaining external state. Desktop integration uses the standard Electron shell.