demo.mp4
Mochi is an AI desktop pet / desktop mascot driven by a multimodal LLM. A transparent, always-on-top pixel cat that sees your whole desktop β window layout, cursor, the active app β and a single vision LLM (local Ollama, or Gemini / OpenAI / Anthropic / any OpenAI-compatible endpoint) decides, in character, how it reacts: stalking your cursor, pouncing, perching on a window, napping when you're idle, sulking when poked, demanding pets. It talks like a self-absorbed cat (not an assistant), learns your habits, and its energy / mood / bond persist across sessions β so its personality grows.
Think Shimeji / desktop buddy, but its behaviour is decided by an LLM agent that actually looks at your screen β a virtual pet with a brain.
Keywords: LLM desktop pet Β· AI desktop pet Β· virtual pet LLM Β· multimodal desktop mascot Β· Ollama desktop pet Β· Shimeji-style AI companion Β· Python / PyQt6 Β· Windows.
It's built to just run: a built-in placeholder cat animates before you add real art, and it falls back to simple rule-based behaviour if the LLM is unreachable. Nothing hard-crashes.
- Sees your desktop β a whole-screen screenshot and a distilled text scene go to the LLM each decision. It genuinely reacts to what's on screen (begs at cat food, hunts a bird in a video), recognises any app by name from its title (Photoshop, Discord, a gameβ¦), reads what you're doing (coding / watching / gaming / away), the time of day, and what just changed ("you just switched to Brave").
- Moves with intent β walks to your cursor, chases/pounces the "little arrow", perches on a window edge, naps on the taskbar, hides, follows you around.
- Talks like a cat β short, playful, self-centred lines (demands, judgement, drama) β never an assistant. Mostly silent, occasionally vocal.
- Expresses itself β picks from 27 expressive animations (nod, sneak, knead, dig, dance, play-dead, tilt-headβ¦) to match its mood.
- Remembers β learns which apps you live in, recalls being petted / poked / thrown, levels up. Energy / mood / bond survive restarts and feed its behaviour.
- Reacts to touch β pet it, poke it, grab-and-throw it (see below).
- Obeys physics β thrown, it falls, bounces, and lands on a window top or the taskbar.
The pet's scope is body-only and safe: it moves and animates itself and pretends to interact. It never injects clicks or keystrokes into your apps.
Sensors (ms, Win32) ββΊ WorldState ββΊ [trigger] ββΊ ONE multimodal LLM
β² β JSON intent
Memory (SQLite) βββββββ βΌ
Body + Render (60fps) executes it
- Sensors read window rects / cursor / foreground app every few ms.
- The brain (an LLM) wakes on a heartbeat or a real change, looks at a screenshot + a distilled scene + recalled memories + its own recent actions, and emits ONE structured intent.
- The body executes that intent as smooth 60fps motion + animation. It re-resolves the target every frame, so the pet tracks a window even while you drag it.
- Reflexes (pet / poke / grab / throw) bypass the LLM for instant response.
The LLM runs on its own thread β the render loop never blocks on it.
To keep a small local model reliable, the model's output is deliberately tiny β it only decides behaviour. Everything mechanical (pixel math, memory, persistence) is handled by code, not the model.
| field | meaning |
|---|---|
verb |
the action β walk_to, follow_cursor, chase, sit_on, watch, nap, nudge, pounce, look_at, hide, idle, say, emotion |
target |
where, by name β cursor, active_window, window:Chrome, taskbar, β¦ (code resolves to pixels) |
edge |
for sit_on: which side of the window to perch on |
emotion |
its mood β happy, curious, annoyed, sleepy, mischievous, β¦ |
emote |
optional expressive animation (one of 27), to match the moment |
say |
a short spoken line, or null (silence is normal) |
thought |
a private one-liner (logged, never shown) |
That's it β no "remember", no confidence, no coordinates. The model just is the cat.
| Input | What it gives the cat |
|---|---|
| Screenshot (whole desktop) | Real sight β reacts to food, prey, animals, anything visible. |
| Window titles | Recognises any app by name via the LLM's own knowledge (not a fixed list). Off-switch: [perception].share_titles. |
| Activity | Coarse read of what you're doing β coding / watching / gaming / browsing / away. |
| Time & context | Time of day, "glued to the same app for an hour", low energy β so it gets clingy late at night, bored in long sessions. |
| "Just happened" | The event that woke it β app switch, clipboard copy, you went idle β so it reacts rather than just describes. |
| Memories | Relevant facts + past episodes recalled by current app + recency. |
| Its own recent actions | The last few turns, so it doesn't repeat itself. |
Cheap structured signals (active app, idle, cursor) come from Win32 every few ms; the screenshot is captured fresh per decision.
python -m pip install -r requirements.txt
python run.pyThat's it. With no art and no LLM configured it runs the placeholder cat with rule-based behaviour. To give it a real brain, point it at an LLM (below).
The project ships configs for three cats β one per sprite colour (Ginger, Grey, Grey-White). Each cat has its own memory DB and personality so they develop independently.
Just double-click start_mochis.bat β it launches all three as
detached background processes. They survive closing the terminal, and you can
use the same file after a reboot.
| Cat | Config | Sprite |
|---|---|---|
| π Ginger | config_ginger.toml |
Cat_Ginger.png |
| β¬ Grey | config_grey.toml |
Cat_Grey.png |
| π± Grey-White | config_grey_white.toml |
Cat_Grey_White.png |
To stop all three at once, double-click stop_mochis.bat.
Under the hood, each cat is launched via a .vbs script that sets the
DESKPET_MANIFEST env var (to pick the sprite colour) and the --config
flag (to pick the config file). You can also launch cats individually:
cscript //nologo start_ginger.vbs
cscript //nologo start_grey.vbs
cscript //nologo start_grey_white.vbsAll five providers are supported and switchable. Configure via config.toml
(copy config.example.toml) or environment variables (env always wins).
| Provider | Notes |
|---|---|
| ollama (default) | Local, free, private β screenshots never leave your machine. Model gemma4:12b (multimodal). |
| gemini | Cloud multimodal. Set GEMINI_API_KEY. |
| openai | gpt-4o / gpt-5-style. Set OPENAI_API_KEY. |
| anthropic | Claude. Set ANTHROPIC_API_KEY. |
| openai_compat | Any OpenAI-compatible endpoint (vLLM / LM Studio / proxy). Set base_url. |
Examples:
:: cloud (Gemini)
set DESKPET_LLM_PROVIDER=gemini
set GEMINI_API_KEY=...your key...
python run.pyIf Ollama runs on another machine, bind it to all interfaces on that box and allow the port through its firewall:
OLLAMA_HOST=0.0.0.0:11434 ollama servethen point the pet at it:
set DESKPET_LLM_PROVIDER=ollama
set DESKPET_LLM_MODEL=gemma4:12b
set DESKPET_LLM_BASE_URL=http://192.168.1.50:11434 :: <- the box's IP
python run.pyOnly expose Ollama on a network you trust. If the pet can't reach the brain, it prints a friendly note and runs rule-based until the brain is back.
Three mouse gestures over the cat (clicks elsewhere pass through to your apps as normal):
- Pet β hover over the cat and move the cursor back and forth across it. It loves it: gets affectionate, kneads, sometimes purrs, and your bond grows fastest.
- Poke β a quick click (tap, no drag). Annoys it β it flinches and grumbles.
- Grab & throw β click-drag the cat, then release to fling. It falls, bounces, and lands on a window top (if it's over one) or the taskbar. Window tops are one-way platforms: it lands on them but passes through their sides.
The cat remembers being petted / poked / thrown, and these touches nudge its mood and bond.
Mochi uses the Bow.Pixel "Cat 85+" sprite pack β all ~92 animations are wired up, via four routes:
- Verbs β motion (walk, run, sit, sleep, pounce, nudge, fallβ¦)
- Emotions β resting pose (content, grumpy, sleepy, curiousβ¦)
- 27 LLM-pickable emotes β the expressive/dramatic ones (nod, shake-head, tilt-head, scratch, dig, eat, knead, dance, sneak, hiss, flinch, play-dead, climb, β¦)
- Random variant pools β idle/walk/run/sit rotate through their variants so it never looks looped or identical between runs.
The placeholder cat is intentionally simple. To use the real pack:
- Download Bow.Pixel "Cat 85+" (free / pay-what-you-want): https://bowpixel.itch.io/meow-cat-85-animation
- Unzip into
assets/(it ships.aseprite+ matching.pngsheets, 3 colours:Cat_Ginger,Cat_Grey,Cat_Grey_White). - Auto-generate the manifest from the aseprite tags (maps every animation to its
exact frames in the sheet, handling the pack's row-aligned export):
(Swap
python -m deskpet.tools.aseprite_to_manifest \ assets/Cat_85_Animations/Cat_Ginger.aseprite \ assets/Cat_85_Animations/Cat_Ginger.png \ --out assets/anim_manifest.yaml \ --sheet-rel Cat_85_Animations/Cat_Ginger.png
Cat_GingerβCat_Grey/Cat_Grey_Whitefor a different colour.) - Relaunch β it auto-detects the art and replaces the placeholder.
State lives in deskpet.db (SQLite). The LLM never decides what to remember β
code does, so the model stays focused on behaviour. Two stores:
-
memoriesβ two kinds, written automatically:- Facts about you β which apps you live in, what you do (watches a lot of videos, codes a lot), being petted / poked / thrown, level-ups. Deduped so the same fact doesn't pile up across sessions.
- Episodes β the cat's own recollections of notable moments (what it saw + did + felt, e.g. "those kibble pictures on screen are torture; I must be fed"), captured on emotional turns and throttled so they don't flood.
Both are retrieved by relevance (current app + recency) back into the prompt, so the cat acts on what it knows and can call back to what it's lived through ("you teased me with food earlier β I haven't forgotten").
-
pet_stateβ energy, mood, bond, level, xp. Energy drains awake and regens while napping; mood eases to neutral; bond/xp climb with positive interaction. Saved periodically (survives a hard kill) and reloaded on start β so the personality drifts and persists across sessions.
Run with INFO (default) for a readable per-decision block:
ββ wake [heartbeat:changed] via LLM
β sees: app=Code.exe[code] cursor=(500,400) NEAR-pet idle=8s energy=0.84(ok) mood=content
β does: chase β cursor feeling mischievous emote=pounce
β π¬ "Tiny arrow, prepare for ambush."
β π§ the cursor skitters; ideal practice for my pounce
ββ
via LLM vs via instinct (rule-based) tells you whether the brain is actually
connected. Run with DEBUG to also see the exact scene sent, the raw LLM JSON,
the screenshot size, and recalled memories:
:: PowerShell
$env:DESKPET_LOG_LEVEL="DEBUG"; python .\run.py
:: cmd
set DESKPET_LOG_LEVEL=DEBUG
python run.pySee config.example.toml for every option. Highlights:
[vision]βenabled,mode(monitor= whole desktop, the default; oractive_window),max_edge(downscale for token cost).[llm]β provider / model / base_url / api_key / temperature.[perception].share_titlesβ send raw window titles to the LLM (default on; see Privacy).[memory],[triggers],[persona],[render].
Key env overrides (env always wins):
DESKPET_LLM_PROVIDER DESKPET_LLM_MODEL DESKPET_LLM_BASE_URL DESKPET_LLM_API_KEY
DESKPET_CONFIG (path to config.toml) DESKPET_LOG_LEVEL (DEBUG|INFO|...)
With the default local Ollama, the screenshot and window titles never leave your
machine. The app never writes screenshots to disk, stores only a hash of the
clipboard (to detect change, never the content), and the model's private thought
is shown only in your local logs.
Cloud providers necessarily receive the screenshot (your whole desktop) β and the
window titles, which add nothing beyond what the screenshot already shows. So
real privacy = local Ollama, where everything stays on-device. If you must use
a cloud provider but want to send less, set [perception].share_titles = false
(only meaningful in text-only mode, since the screenshot otherwise dominates).
The brain, memory, parsing, triggers, sprite, body, and physics logic are cross-platform and unit-tested (they run on Linux/Mac); only the perception and window layers require Windows.
pip install -r requirements.txt
pytest -q # logic tests
python -m deskpet.brain.agent --once # one brain decision (needs an LLM)The pet's behaviour scope is body-only β it moves and animates itself and pretends to interact. It never injects clicks/keystrokes into your other apps.