Miso Chat is a roleplay chat app. It talks to Gemini by default, and can also use DeepSeek, a local server that speaks the OpenAI chat format, or the claude CLI. It has two front ends:
roleplay.py, a terminal chat with one character or a group.web/, a FastAPI web app for several accounts. Each account has its own chats, memory and persona. The admin manages accounts, usage caps, token budgets, providers, character cards and icons, and has a stats page.
This is a personal project, built for one machine or a private network. It is not hardened for the open internet. See Security below.
- Python 3.11 or newer. The code was tested with Python 3.11 on Windows.
- A Gemini API key. Optional: a DeepSeek key, a local OpenAI-style server, or the
claudeCLI signed in to an account.
python -m venv .venv
.venv\Scripts\activate (Windows)
source .venv/bin/activate (macOS or Linux)
pip install -r requirements.txt
copy .env.example .env (or: cp .env.example .env)
Open .env and set GEMINI_API_KEY. The app reads .env on start-up. A value already set in the environment wins over the file.
python roleplay.py Cliff
python roleplay.py Cliff Cayprae
python roleplay.py --group MyGroup Cliff Cayprae
The second name is the persona folder you speak as. Inside a chat:
/modellists the models./model <name>pins one, and/model autogoes back to the fallback chain./saveupdates the memory bank now./backremoves the last exchange./historyprints the whole chat./syncpulls chats from other machines on your network. See Peer sync below./verbosetoggles diagnostics./quitor/exitends the session and saves the memory bank.
python -m uvicorn web.app:make_app --factory --host 127.0.0.1 --port 8000
Open http://127.0.0.1:8000 in a browser. The first login is admin with the password admin, and the app asks you to change it before anything else. Create other accounts from the admin page.
Run the web app from the project folder, because character folders are read relative to it.
What the web app can do:
- Edit any saved message. Editing does not call the model.
- Regenerate a reply and swipe between the saved versions.
- Keep a rolling window of recent messages (1 to 100, set in Settings). Older messages are folded into a running summary during the memory refresh. The summary is shown in the Memory panel and can be edited there.
- Set a creativity (temperature) value for each character in Settings, from 0 to 1.5.
- Export a chat as Markdown or plain text.
- Turn on a character sandbox, a private folder the model can read and write through a few file tools. It is off by default. Turn it on by putting
{"enabled": true}in asandbox.jsonfile in the character folder. Admins can browse sandboxes read-only. - Switch between a dark theme (the default) and a light theme with the toggle in the header.
| Name | Purpose |
|---|---|
GEMINI_API_KEY |
Required for Gemini models. Read from .env or the environment. |
MISO_DATA_DIR |
Where app.db, providers.json and per-user chat state live. Default: ./data. |
MISO_COOKIE_SECURE |
Set to 1 when the web app is served over HTTPS, so the login cookie is marked Secure. |
The older names ROLEPLAY_DATA_DIR and ROLEPLAY_COOKIE_SECURE are still read.
DeepSeek, a local OpenAI-style server and Claude Code are set up on the admin page, under Model providers. Keys are write-only: the browser sees a masked hint only. They are stored in data/providers.json, with owner-only permissions where the system supports them.
Model ids for these providers carry a prefix: deepseek:..., local:... and claude:.... The claude: prefix runs the claude CLI installed on this machine, using the account it is signed in to.
The Gemini models the app will use are listed in ALLOWED_MODELS in rp_core.py, and the default fallback chain is DEFAULT_FALLBACKS. Only ids in ALLOWED_MODELS are used. Provider model names change over time, so check them against the provider's documentation before you rely on them.
Each character is a folder in the project root.
character.mdholds the card. The first line isCharacter Persona: <Name>. A lineColour: <name or #hex>sets the colour of the name badge and bubble.context.md(optional) holds memories, places and situations, and is added to the prompt.icon.png,icon.jpgoricon.webp(optional) is shown beside the name. Without one, the app shows a coloured initial.- Names can hold any characters, such as
Dr. Fox: Night Form. Windows folder names cannot hold< > : " / \\ | ? *or%, so those are percent-encoded in the folder name:Dr. Fox: Night Formlives in the folderDr. Fox%3A Night Form. The name shown in chat comes from the folder name, or from theCharacter Persona:line when there is one. memory.jsonandmemory.mdare written by the app as the chat runs. Do not edit them while a chat is open.
A group chat is a folder with a group_config.json that lists its members.
personas.json lists the folders you can speak as. A persona is never offered as a character to chat with. Cayprae is included as an example persona, and Cliff as an example character.
An optional substitutions.json in the project root maps the text you type (the key) to the text the model receives (the value). This copy ships with no substitutions.
{
"[Big Shot!]": "big shot!",
"[For 9.99]": "for nine ninety-nine",
"[Spamton](https://deltarune.fandom.com/wiki/Spamton)": "Spamton"
}
How it works:
- The model receives the value. The chat shows the key. Model replies are converted from values back to keys before they are displayed, so the key's formatting, brackets and links appear in the chat.
- Matching is case-insensitive, whole-word and longest key first. A key only matches when it isn't touching another letter or digit.
- Keys can hold punctuation, brackets and markdown. Model replies are rendered as markdown, and links must be
httporhttps, so a key can be a link. - Saved history keeps the values, so the links are not sent back to the model.
- Sessions read the file when they start. Restart the terminal chat, or restart the web server, after editing it.
Ideas for using it:
- Stage tags:
[Big Shot!]shows in the chat as a bracketed tag, while the model only sees a plain phrase. - Character links: wrap a character's name as a markdown link to its wiki page, so the model's mentions become clickable.
- Catchphrase formatting: keep a character's signature line in its exact capitals or punctuation in the display, while the model reads a plainer version.
- Prices and numbers:
[For 9.99]can show in a shop line while the model reads the spelled-out form.
Watch out for:
- Values are replaced inside text, including inside URLs that were inserted by an earlier key. Pick values that don't appear in any link target. For example, a value of
wikiwould also rewrite thewikiindeltarune.fandom.com/wiki/.... - A value that is a common word (
the,a) will match everywhere. Keep values specific.
Sync lets you copy chats and character folders between two machines on the same local network. It is manual.
- The terminal client listens on port 54322 (TCP and UDP) while it runs, so the other machine can find it. Nothing syncs at start-up. Sync runs only when you type
/sync. /synclooks for other copies on the local subnets and syncs with the ones it finds. Add addresses by hand in apeers.jsonfile (a list of"host"or"host:port"entries) if broadcast is blocked, or type/sync <address>.- Files are copied with a backup (
.p2pbak) before they are overwritten. peers.jsonis a per-machine file. It is git-ignored and is not part of this repository.
Use sync only on a network you trust. It has no encryption and no login.
data/ holds the database, accounts, sessions, per-user chat history and memory, and provider keys. .env holds your Gemini key. Both are git-ignored. Keep them out of any repository or shared folder.
The repository does not include personal character folders, memory files, peer lists, usage trackers or your provider keys. Only the two example characters are included.
- Use the web app on a private network, or behind a private tunnel or VPN. Set
MISO_COOKIE_SECURE=1when you serve it over HTTPS. - Change the admin password on first login. Use long, unique passwords for every account.
rp_core.pysets the safety thresholds insafety_settings()toBLOCK_NONEfor sexual content, harassment, hate speech and dangerous content. Review them before you use the app, and check what your provider allows. You are responsible for the content you generate and send.- Costs are billed by each provider. Claude Code uses the quota of the account signed in to the
claudeCLI.
This repository does not include an automated test suite. A basic static check is:
pip install pyflakes
python -m pyflakes roleplay.py rp_core.py llm_backends.py dotenv_loader.py web
- Gemma 4 31B on the free tier has a small input limit (16K tokens a minute), so a large prompt can be refused. Use a billed key or a Flash-Lite model for long chats.
- The rolling summary folds in at most 60 messages per memory refresh. If more than 60 messages leave the window between refreshes, the oldest are skipped.
The version is in VERSION, and every release is listed in CHANGELOG.md. Each release is tagged on GitHub (for example v1.1.0).
- Miso Chat is released under the MIT licence. See
LICENSE. - Merriweather, Inter and JetBrains Mono are in
web/static/fonts. Merriweather is under the SIL Open Font License 1.1 (web/static/fonts/OFL.txt). Inter and JetBrains Mono are under the same licence (web/static/fonts/OFL-inter-jetbrains-mono.txt).