Working chat: auth, connections, streaming, folders
LLeMbas now runs end to end. Register, add an OpenAI-compatible connection, and hold a real streaming conversation organised into folders. Verified against the local llama-swap instance. Streaming is the one genuinely tricky part. Sending a message returns two HTML fragments -- the user bubble and an empty assistant bubble carrying an sse-connect -- and that attribute is the ONLY thing that starts a generation. Rendering an incomplete assistant message as a streaming shell falls out of the same template, which means loading a page whose last reply never finished simply picks it up again. Details worth knowing about, each commented where it matters: - SSE payloads are split across several data: lines. A raw newline in one data: line truncates the event, which shows up the first time a model emits a code block. - Markdown is rendered server-side by the same helper for both the page and the final streamed frame, so the two cannot disagree. The fence renderer is replaced outright rather than using markdown-it's highlight option, which re-wraps output in a second <pre>. - escape_text is html.escape, not nh3.clean_text: it escapes character by character, so escaping stream chunks separately equals escaping the whole string. - The stream opens its own session via session_scope(); it outlives the request handler and the dependency-scoped session may be closed. - Deleting a folder keeps the chats inside it (FK is SET NULL). Losing a conversation to a mis-clicked folder delete is unforgivable. - Login failures use one message for "no such account" and "wrong password" so the form cannot enumerate registered addresses. Also adds deploy/ for the gamebox install at https://chat.lan: system unit, nginx vhost with buffering off (buffering on turns streaming into one lump at the end), and install/update scripts following the same service-user and /srv bind-mount conventions as llama-swap and comfyui. 70 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,154 @@
|
||||
# LLeMbas
|
||||
<p align="center">
|
||||
<img src="assets/banner.svg" alt="LLeMbas — waybread for the long road of thought" width="100%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>A self-hosted web UI for your language models, written in Python.</strong><br>
|
||||
Talks to anything that speaks the OpenAI API. Themed after Middle-earth.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-3E6B7A?style=flat-square">
|
||||
<img alt="License GPL-3.0" src="https://img.shields.io/badge/license-GPL--3.0-C9A227?style=flat-square">
|
||||
<img alt="No Node required" src="https://img.shields.io/badge/build%20step-none-6B8E4E?style=flat-square">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
*Lembas* is the Elvish waybread — one bite sustains a traveller for a day's
|
||||
march. The capitals hide what it runs on: **LLeM**bas.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Most self-hosted LLM front-ends are large JavaScript applications with a Python
|
||||
API bolted underneath. LLeMbas is the other way round: **server-rendered
|
||||
Python**, with htmx and a little Alpine for interactivity. There is no
|
||||
`package.json`, no bundler, no build step, and nothing is fetched from a CDN at
|
||||
runtime. Clone it, `pip install -e .`, run it.
|
||||
|
||||
## Features
|
||||
|
||||
**Working now**
|
||||
|
||||
- **Chats** — streaming replies, Markdown with server-side syntax highlighting,
|
||||
copy and regenerate, automatic chat titles
|
||||
- **Folders** — arbitrarily nested, delete a folder without losing the chats
|
||||
inside it
|
||||
- **OpenAI connections** — point at OpenAI, LM Studio, vLLM, llama.cpp,
|
||||
llama-swap, Ollama or OpenRouter; models are discovered and cached
|
||||
- **Accounts** — first account becomes the administrator, argon2 password
|
||||
hashing, revocable server-side sessions
|
||||
- **Two themes** — *Moria* (dark) and *Shire* (light), switchable per user
|
||||
|
||||
**Planned**
|
||||
|
||||
Users & groups with permissions · file upload, vision and PDFs · built-in tools
|
||||
with admin settings · custom tools and MCP servers · agentic execution (local
|
||||
and over SSH) · image generation.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone https://git.houmeres.sk/Houmeres/LLeMbas.git
|
||||
cd LLeMbas
|
||||
|
||||
python -m venv .venv && . .venv/bin/activate
|
||||
pip install -e ".[dev]"
|
||||
|
||||
cp .env.example .env
|
||||
lembas secret-key # paste the result into LEMBAS_SECRET_KEY
|
||||
|
||||
lembas serve # http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
Open the address and create the first account — it becomes the administrator.
|
||||
Then go to **Admin → Connections** and add an endpoint. For a local runner that
|
||||
is usually `http://localhost:1234/v1` with no API key. Press **Test & refresh**
|
||||
and its models appear in the chat model picker.
|
||||
|
||||
> The vendored browser libraries (htmx, Alpine) are committed, so no network
|
||||
> access is needed to run. To re-fetch or bump them:
|
||||
> `python scripts/fetch_vendor.py --update`.
|
||||
|
||||
## Configuration
|
||||
|
||||
All variables are prefixed `LEMBAS_` and can live in `.env`. See
|
||||
[`.env.example`](.env.example) for the annotated list.
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `LEMBAS_SECRET_KEY` | *generated* | Signs sessions and encrypts stored API keys. **Set this.** A generated key changes every restart, signing everyone out and making stored API keys unreadable. |
|
||||
| `LEMBAS_DATA_DIR` | `./data` | SQLite database and uploads. |
|
||||
| `LEMBAS_HOST` / `LEMBAS_PORT` | `127.0.0.1` / `8080` | Bind address. |
|
||||
| `LEMBAS_ALLOW_SIGNUP` | `true` | Let new users register themselves. The first account is always an admin regardless. |
|
||||
| `LEMBAS_DEFAULT_THEME` | `moria` | `moria` (dark) or `shire` (light). |
|
||||
| `LEMBAS_SESSION_TTL` | `2592000` | Session lifetime in seconds. |
|
||||
| `LEMBAS_REQUEST_TIMEOUT` | `300` | Seconds to wait on an upstream model. |
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
lembas serve # run the server
|
||||
lembas info # where data lives, what is configured
|
||||
lembas secret-key # generate a value for LEMBAS_SECRET_KEY
|
||||
lembas create-admin # create or promote an administrator
|
||||
```
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
Browser ──form POST──▶ FastAPI ──▶ SQLite
|
||||
▲ │
|
||||
│ └──httpx──▶ any OpenAI-compatible endpoint
|
||||
└──── server-sent events ◀───────────────┘ (streamed reply)
|
||||
```
|
||||
|
||||
Sending a message stores the turn and returns two HTML fragments: the user's
|
||||
bubble and an empty assistant bubble carrying an `sse-connect`. That opens a
|
||||
server-sent event stream which appends tokens as they arrive, then replaces the
|
||||
whole bubble with the finished, Markdown-rendered version. Rendering and
|
||||
highlighting happen in Python, so the streamed and final views cannot disagree.
|
||||
|
||||
```
|
||||
src/lembas/
|
||||
api/ routes: auth, chats, folders, admin, pages
|
||||
db/models/ SQLAlchemy schema
|
||||
security/ password hashing, sessions
|
||||
services/ llm client, chat orchestration, markdown, crypto, sse
|
||||
web/ Jinja templates and static assets
|
||||
assets/ SVG artwork masters
|
||||
scripts/ artwork generator, vendored-JS fetcher
|
||||
deploy/ systemd unit and nginx vhost for a real install
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pytest # test suite
|
||||
ruff check . # lint
|
||||
python scripts/build_artwork.py # regenerate the SVG artwork
|
||||
python scripts/fetch_vendor.py # verify vendored JS against the lockfile
|
||||
```
|
||||
|
||||
There is no migration tool. The schema is SQLite-only and created at startup,
|
||||
so changing a column on a live database is a manual job — see `CLAUDE.md`.
|
||||
|
||||
## Artwork
|
||||
|
||||
The logo, favicon and banner are original vector work, generated by
|
||||
[`scripts/build_artwork.py`](scripts/build_artwork.py) so the mallorn leaf stays
|
||||
identical across every size it appears at. The wordmark is
|
||||
[Source Serif 4](https://github.com/adobe-fonts/source-serif) (SIL OFL 1.1)
|
||||
converted to outlines — a README banner cannot load a webfont, and `<text>`
|
||||
would render in whatever serif the reader happens to have.
|
||||
|
||||
## Licence
|
||||
|
||||
[GPL-3.0](LICENSE).
|
||||
|
||||
## A note on the theme
|
||||
|
||||
This is an independent hobby project, themed as an affectionate nod to
|
||||
J.R.R. Tolkien's world. It is **not affiliated with, endorsed by, or connected
|
||||
to** the Tolkien Estate, Middle-earth Enterprises, or any related rights
|
||||
holder. All artwork here is original.
|
||||
|
||||
Reference in New Issue
Block a user