NIRA is a privacy-first personal AI assistant that bridges natural-language conversation with real tools and a workspace for your work. It runs a FastAPI backend and a polished, installable web UI (PWA) so it feels like a native desktop app. NIRA can talk β with natural, sentence-level streaming voice β and it can act: browse the web, run terminal commands, read/write files, check the weather, and search the internet, all orchestrated by an LLM through OpenRouter.
Built by Robel Biruk β pharmacy student and software developer passionate about AI, automation and human-centered interfaces.
- Multi-step tool orchestration β NIRA automatically decides which tools to call and chains them to complete a request.
- Streaming responses β live Server-Sent Events (SSE) token/state streaming, with a visible "thinking β executing β speaking" core ring.
- Conversation memory β every chat is saved as a named session. Memory is local-first (browser
localStorage, so it always survives reloads) and synced to Firebase Firestore per anonymous user for cross-device access. - Inline session management β rename a session by clicking its title, delete via a confirmation popover (no browser alerts).
- Personalized greeting β NIRA asks your name once (first-run modal) and greets you by time of day ("Hello you" for new users, "Hello {name}" after).
- Automatic model fallback β when the active model hits a rate limit, NIRA seamlessly switches to another available free model.
- Speech-to-text β tap the core / mic to dictate; your speech is transcribed and sent as a message.
- Natural text-to-speech β replies are spoken with sentence-level streaming via the browser's Web Speech API (keyless, no external dependency), prefetching the next sentence so there is no robotic pause between phrases. Tap the core to interrupt speech.
- Voice toggle in the activity panel; mic state shown on the core ring.
- Group everything related to one goal into a Project: chats, memories, notes and research.
- Create projects with a name, emoji icon and description.
- Each project card shows π¬ chats / π§ memories / π¬ research counts + last active.
- Tag the current chat to a project from the Project dropdown in Chat, so it appears under that project.
- Projects persist locally and sync to Firebase (under
users/{uid}/projects).
| Tool | Description | Example |
|---|---|---|
open_browser |
Open a URL in your default browser | "Open GitHub" |
run_terminal_command |
Execute a shell command | "What's my IP?" |
read_file / write_file |
Read or create/modify files | "Create a notes file" |
list_directory |
List a directory's contents | "What's in Documents?" |
get_weather |
Current weather + forecast for a city (Open-Meteo with wttr.in fallback) | "Weather in Paris?" |
web_search |
Web search (Tavily, key from TAVILY_API_KEY env) |
"Find Python tutorials" |
browser |
Browsing surface driven by the browser tool | "Summarize this page" |
Tools can also be invoked directly with slash commands (e.g. /tools, /clear, /model, /help) which bypass the LLM and call the backend.
- Chat β the main conversation with the holographic AI core ring.
- Memory β all saved sessions; resume, inline-rename, or delete.
- Projects β workspace: create projects, see conversations/memories/notes/research grouped by project.
- Browser β web browsing surface driven by the browser tool.
- Research β research/summarization workflow.
- About β project info: version, live statistics, creator, roadmap, license, contributing, and a scrolling footer marquee.
- Settings β switch models, add/remove AI providers (OpenRouter + custom providers), and set per-tool API keys (Google, GitHub, Spotify, Tavily, β¦).
A snapshot of the capabilities added in the latest releases β smarter research, secure account connections, and a transparent "thinking" layer.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NIRA β New Capabilities β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββ β€
β π Web β Smart research agent β reads real pages, not just β
β β snippet dumps. Streams its steps, then answers. β
β π OAuth β One-click Connect for Google (Gmail), GitHub, β
β β Spotify β per-user tokens, nothing shared. β
β π‘ Reasoning β Live "Thinkingβ¦" stream shown before the answer. β
β π§ Memory β Local-first + Firebase sync, fully private. β
ββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The web tool runs an autonomous fetch-based research loop: it searches, opens
the most relevant result, reads the page, and synthesises a sourced answer β
all within a 512 MB cloud footprint (no headless Chromium required).
ββββββββββ search ββββββββββββ pick + fetch βββββββββββββ
β Query β βββββββββββΊ β Search β ββββββββββββββββΊ β Content β
ββββββββββ β (Tavily β β Page β
β² β β DDG β β βββββββ¬ββββββ
β β Wiki) β β read
β answer + ββββββββββββ βΌ
β source ββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββ Summarise β
ββββββββββββββ
Output: [web Β· fetch Β· N steps] + answer + π source link
- Resilient search chain β Tavily (datacenter-friendly) β DuckDuckGo HTML β Wikipedia fallback, so it always returns something useful.
- Never loops β a step-limit safety net summarises the best page it found instead of failing.
- Honest β cites the page it read; says so when a source doesn't fully cover the question.
Connect real accounts from Settings β Tool Connections β each user's tokens are isolated (no shared credentials), and every tool returns actionable errors (exact console fix) instead of raw exceptions.
User ββConnectβββΊ OAuth Consent ββtokenβββΊ Encrypted per-user store
β
βββββββββββββββββββ¬ββββββββββββββββΌββββββββββββββββ
βΌ βΌ βΌ βΌ
π§ Gmail π GitHub π§ Spotify π API keys
(read/summarise) (search/repos) (search/play) (Tavily, β¦)
| Connection | Unlocks | Notes |
|---|---|---|
gmail β list / read / summarise mail |
Add yourself as a Test User if the app is unverified | |
| GitHub | repo & code search | Fine-grained token scopes |
| Spotify | search & playback control | Auto token via Client Credentials |
| API keys | Tavily and other keyed tools | Env vars win over Settings |
With a reasoning-capable model (e.g. deepseek/deepseek-r1:free,
qwen/qwq-32b:free), NIRA shows its work before the answer β the wait
becomes a live, pulsing "π‘ Thinkingβ¦" stream that quietly collapses once the
reply begins.
βββ while reasoning βββ βββ once answering βββ
β π‘ Thinkingβ¦ β βββββββΊ β βΈ π‘ Reasoning β (collapsed)
β Β· streams live β answer β ββββββββββββββββββ β
β Β· gently pulses β starts β Answer leads β΄ β
βββββββββββββββββββββββ ββββββββββββββββββββββ
- Captured from native
reasoningfields and inline<think>tags. - Purely additive β non-reasoning models simply don't show the block.
NIRA ships a favicon set (not a single file) so every device picks the right
resolution. They live in ui/public/ and are referenced from ui/index.html:
| File | Size | Used for |
|---|---|---|
favicon-16.png |
16Γ16 | Small browser-tab icons, bookmark bars |
favicon-32.png |
32Γ32 | Standard desktop tab / taskbar |
favicon-70.png |
70Γ70 | Windows tiles / medium shortcuts |
favicon-96.png |
96Γ96 | High-DPI tabs, Android home-screen, apple-touch-icon |
The browser's <link rel="icon" sizes="β¦"> + srcSet/sizes attributes let it
choose the closest size for the current device pixel ratio β e.g. a 2Γ Retina tab
uses favicon-32.png (rendered at 16 CSS px), a 3Γ phone uses favicon-96.png.
The same set is reused in manifest.webmanifest (maskable + any-purpose) so the
installed PWA icon looks crisp on phones and tablets. To change the logo, replace
all four PNGs at the sizes above (keep the names) β no code change needed.
- Installable: "Install app" / "Add to Home Screen" from any modern browser.
- Ships a
manifest.webmanifest(display: standalone,orientation: any) and a service worker (sw.js) that caches the production bundle for offline use. - The service worker is registered only in production builds (dev keeps HMR clean).
- Model selection at runtime; any OpenRouter-compatible model works.
- Custom providers β add your own OpenAI-compatible endpoints in Settings.
- Tool API keys β supply keys for external tools (e.g. Tavily) via the
TAVILY_API_KEYenv var or Settings; environment variables take priority. - Feature toggles β enable/disable capabilities from the activity panel (persisted server-side).
- Adding a tool is a matter of subclassing
Toolintools/and registering it.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NIRA Architecture β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β β Web UI β β FastAPI β β Assistant β β
β β (React) βββββΊβ Backend βββββΊβ (Core Brain) β β
β β (PWA/Vite) β β app.py β β core/assistant.py β β
β βββββββββββββββ βββββββββββββββ ββββββββββββ¬βββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββΌβββββββββββ β
β β AI Client (OpenRouter) β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββΌββββββββββββββββββββββββ β
β β β β β
β ββββββββββΌβββββββββ ββββββββββΌβββββββββ βββββββΌβββββββ β
β β Memory β β Tool Manager β β Runtime β β
β β (localStorage + β β (router/registry)β β (State) β β
β β Firestore) β β β β β β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββ β
β β β β
β ββββββββββΌβββββββββ ββββββββββΌβββββββββ β
β β Preferences β β Tool Registry β β
β βββββββββββββββββββ ββββββββββ¬ββββββββββ β
β β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β Browser β β Terminal β β Weather β ... β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
User Request β FastAPI Endpoint β Assistant β AI Model + Tools β Response
β β
Streaming (SSE) Memory / Speech
β β
Web UI (PWA) βββββββββββββββββββββββββββ
Memory storage: chat sessions and the user's name are stored local-first in localStorage (guaranteed to work, survives reloads) and mirrored to Firebase Firestore (users/{uid}/sessions, users/{uid}/projects) when an anonymous account is available. Anonymous auth + per-uid Firestore rules keep data private.
- Python 3.11+
- Node.js 18+
- Git
git clone https://github.com/Robibiruk/Nira-AI-Assistant.git
cd Nira-AI-Assistantpython -m venv .venv
# Windows
.\.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
pip install -r requirements.txtGet a free key at OpenRouter and set it:
# PowerShell
$env:OPENROUTER_API_KEY="sk-or-..."
# macOS / Linux
export OPENROUTER_API_KEY="sk-or-..."Or put it in config/settings.yaml under openrouter.api_key (see config/settings.yaml.example).
uvicorn app:app --reloadBackend serves on http://127.0.0.1:8000 (and serves the built UI in production).
cd ui
npm install
npm run dev # http://127.0.0.1:5173 (proxies API to :8000)cd ui
npm run build # outputs ui/dist
cd ..
uvicorn app:app --reload # serves the built PWA at http://127.0.0.1:8000In a supported browser, open the running app and choose Install app / Add to Home Screen. The service worker caches the bundle so NIRA works offline.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| POST | /chat |
Non-streaming chat (final reply) |
| POST | /chat/stream |
Streaming chat (SSE: meta β state β tool_result β message) |
| POST | /tools/run |
Run a tool directly (slash commands) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /models |
List available models |
| POST | /models/select |
Switch active model |
| GET | /providers |
List configured providers |
| GET/POST | /providers/custom |
List / add / remove custom providers |
| GET/POST | /tools/keys |
List / set per-tool API keys |
| GET/POST | /features |
List / toggle feature flags |
| GET | /status |
Runtime status readout |
| Method | Endpoint | Description |
|---|---|---|
| GET | /desktop |
Installed apps, running windows, browser tabs (desktop builds) |
| POST | /desktop/action |
Focus / close / launch a desktop app |
| POST | /speak |
Text-to-speech synthesis (browser TTS on the client) |
Sessions/name are managed client-side (localStorage + Firebase), so there are no
/sessionsserver endpoints β the backend is stateless for chat history.
config/settings.yaml (copy config/settings.yaml.example β config/settings.yaml):
openrouter:
api_key: sk-or-... # or set OPENROUTER_API_KEY env var
model: poolside/laguna-m.1:free
voice: true # enable voice (speech-to-text + TTS)
temperature: 0.7
tools:
enabled: [] # empty = all tools enabled| Variable | Description | Default |
|---|---|---|
OPENROUTER_API_KEY |
OpenRouter API key (required) | β |
TAVILY_API_KEY |
Tavily web-search key (optional; env wins over config) | β |
SPOTIFY_CLIENT_ID |
Spotify app Client ID β enables automatic token fetch (Client Credentials) so Spotify works without manual tokens and survives restarts | β |
SPOTIFY_CLIENT_SECRET |
Spotify app Client Secret (paired with SPOTIFY_CLIENT_ID) |
β |
SPOTIFY_API_KEY |
Optional: a manually-pasted Spotify access token (overrides the above). Short-lived β refresh when requests 401 | β |
UVICORN_HOST |
FastAPI host | 127.0.0.1 |
UVICORN_PORT |
FastAPI port | 8000 |
| Variable | Description |
|---|---|
VITE_API_BASE |
Backend base URL (e.g. https://nira-ai-backend.onrender.com) |
VITE_FIREBASE_API_KEY |
Firebase web API key (the only Firebase value wired to env; others are hardcoded in firebase.js) |
Nira-AI-Assistant/
βββ app.py # FastAPI entry point (also serves built UI)
βββ config.py # Configuration loader
βββ requirements.txt # Python dependencies
βββ ai/ # AI client + model registry
β βββ openrouter.py # OpenRouter client
β βββ provider.py # Provider abstraction
β βββ providers.py # Built-in + custom providers
β βββ models.py # Model ids / defaults
βββ core/ # Assistant brain
β βββ assistant.py # Orchestration (stateless; gets history from client)
β βββ planner.py # Multi-step planning
β βββ prompts.py # System prompt / personality
β βββ runtime.py # Runtime state
β βββ router.py # Tool manager + registry
βββ tools/ # Built-in tools
β βββ base.py # Tool contract
β βββ browser.py # Browser
β βββ files.py # File read/write
β βββ search.py # Web search (Tavily)
β βββ terminal.py # Terminal
β βββ weather.py # Weather (Open-Meteo + wttr.in fallback)
β βββ _keys.py # Per-tool key resolution (env > config)
βββ ui/ # Vite + React PWA frontend
β βββ public/ # manifest.webmanifest, sw.js, icons, Me.jpg, fontawesome/
β βββ src/
β β βββ App.jsx # App shell + routing
β β βββ main.jsx # Entry + SW registration
β β βββ index.css # Global styles
β β βββ api.js # API client
β β βββ firebase.js # Firebase init + per-uid Firestore helpers
β β βββ memoryStore.js # Local-first localStorage persistence
β β βββ utils.js # Shared helpers (date formatting)
β β βββ hooks/ # useNira, useVoice
β β βββ components/ # Chat, Memory, Projects, Browser, Research,
β β # About, Settings, Sidebars, β¦
β βββ package.json
β βββ vite.config.js
βββ config/
β βββ settings.yaml # User config (git-ignored)
βββ firestore.rules # Per-uid Firestore security rules
Completed
- Smart Chat
- Browser
- File Tools
- Research
- Voice Assistant
- Projects (workspace)
- Local-first memory + Firebase sync
Upcoming
- Plugin Marketplace
- Mobile App
- Multi-Agent System
- Smart Home Integration
- File/image attachments inside Projects (Firebase Storage ready)
NIRA works with any free, tool-capable OpenRouter model. Verified (as of July 2026):
| Model | ID | Context |
|---|---|---|
| Laguna M.1 | poolside/laguna-m.1:free |
128,000 |
| Llama 3.3 70B | meta-llama/llama-3.3-70b-instruct:free |
128,000 |
| Gemma 4 31B | google/gemma-4-31b-it:free |
128,000 |
| GPT-OSS 120B | openai/gpt-oss-120b:free |
128,000 |
| Qwen3 Coder | qwen/qwen3-coder:free |
128,000 |
Automatic fallback switches models on rate-limit.
tools/my_tool.pysubclassingTool(fromtools.base):from .base import Tool class MyTool(Tool): name = "my_tool" description = "What it does." parameters = {"q": {"type": "string", "description": "Query"}} required = ["q"] def run(self, q: str) -> str: return f"Result: {q}" my_tool = MyTool()
- Export it in
tools/__init__.pyand add toALL_TOOLSincore/router.py.
Edit core/prompts.py.
Add an OpenAI-compatible endpoint in Settings β Providers (no code needed).
- Terminal execution runs arbitrary shell commands β only enable for trusted local use; restrict via
tools.enabled. - API keys are never committed (see
.gitignore); use env vars orconfig/settings.yaml(git-ignored). - File tools can read/write any file on your system.
- Memory is private β chat history lives in your browser (
localStorage) and your own Firebase account (anonymous auth, per-uid Firestore rules). No shared server database.
NIRA is open source under the MIT License β see LICENSE.md. Contributions are welcome β see CONTRIBUTING.md.
- GitHub: @Robibiruk
- LinkedIn: Robel Biruk
- Portfolio: robel-portfolio-website.netlify.app
Made with π by Robel Biruk β Β© 2026 Nira AI.