a4cfb2eea41bb304025ae235ae8a8a606ab7389a
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2c8c274850 |
Every injected prompt becomes editable, and several get written
The instructions LLeMbas puts in front of a model were hard-coded: six
strings in a GUIDANCE dict, two headings, and the title request inline in
chat.py. An operator could not see what was being sent, let alone change
it, and there was nowhere for a custom tool to contribute its own guidance
when custom tools land.
services/prompts.py now holds each piece as a Fragment, and /admin/prompts
edits them with a preview of the whole assembled system message including
unsaved edits. harness.py keeps only the decisions -- which fragments apply
to this request, and what their variables resolve to.
The design turns on one choice: a fragment carries its gate as data
(families, requires, when_tools) rather than as a callable, because a
database row can carry the same three fields. Custom tools will therefore
register a fragment source and change nothing else -- there is a test that
says exactly that, and it is the reason the rest of the shape is what it is.
Consequences worth knowing:
- Defaults live in code, overrides in the database, and text equal to its
default is never stored. Otherwise pressing Save once would freeze
today's wording forever and no later release could improve it.
- An empty override means off. A fragment that was not submitted at all
keeps what it had, because it may be missing from the page only because
whatever contributes it is currently switched off.
- requires= replaced the hand-written pair of memory guidance variants.
The sentence that refers to a section now lives inside that section, so
it cannot outlive it. That was the general problem the pair was a
special case of.
- {{name}}, with anything unrecognised passing through verbatim. The name
grammar is the guard: {"total": 1} and ${PATH} are not candidates.
Substitution is one pass and never recursive, because {{memories}}
carries text a model wrote.
The wording is also overhauled, and a model now gets the core fragments
even with no tools -- the date above all. "An empty harness is worse than
none" was about tokens that say nothing; a model with no clock being asked
about the present is not that. Clearing those boxes restores the old
silence exactly. New: today's date, who it is talking to, the three-round
tool budget, that tool results are not replayed, that anything a tool
returns is data rather than instruction, and what the <document> wrapper
around an attachment is. Extended: memory_forget, notes_edit/delete,
skill_create/edit, and reading a knowledge document in full rather than
answering from an extract.
Tool descriptions stay in code and are listed read-only. They are schema
and they state facts about what a runner does; an edit would make the text
a lie with nothing to catch it.
No schema change -- one JSON row in the settings table.
488 tests. Version 0.2.0, which also invalidates the service worker cache
so the green artwork appears without a hard reload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f744232d25 |
Fix attachments never being sent with the message
Uploading an image showed the chip and then did nothing: the file was stored but never reached the model. Two causes, both in the composer template. The chips live in #attachments, and each carries the hidden file_ids input that binds it to the message. That container sat OUTSIDE the <form>, with an `hx-include="#attachments"` on a hidden <div> inside the form meant to pull it back in. That attribute only has an effect on the element issuing the request -- on a child of it, it does nothing. So the form serialised content and nothing else, and post_message saw no file_ids at all. Fixed by putting #attachments inside the form, where the inputs are submitted because they are in the form, rather than because of an attribute that has to be wired correctly. The file input stays outside, since inside it would submit an empty file part on every message. Second: /chat preselected models[0] rather than the model a new chat would actually use. With a vision model set as the default and a non-vision one first in the admin ordering, the composer showed the wrong model, sent the wrong model, and told the user images *would* be sent when they would not. It now resolves through default_model(), the same path /start uses. Every server-side test passed throughout, because the bug was entirely in the wiring between template and browser. Added tests that serialise the rendered form the way a browser does -- every named input inside <form> -- and assert file_ids is among them and the image reaches the model as a content part. Verified they fail with the old markup restored, then pass again. Confirmed end to end against gemma4-e4b-q8: given a drawing, it replied "Left: Green Circle / Right: Orange Triangle". 220 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b67568f2c |
Fix bulk actions, create chats lazily, rework the UI
Seven reported problems.
**Bulk model actions 404'd.** /admin/models/{model_id} was registered
before /admin/models/bulk, and FastAPI matches in registration order, so
"bulk" was parsed as a model id. Moved above the parameterised route,
with a comment saying why, and a regression test.
**Empty chats piled up.** There is now no endpoint that creates one.
"New chat" is a link to /chat, which renders a composer with no row
behind it, and POST /api/chats/start writes the chat together with its
first message. Opening one and walking away leaves nothing.
**Pinning meant two different things.** The picker is now always in the
administrator's position order; pinned models get shortcuts in the chat
sidebar and nothing else. A picker whose order silently differs from the
admin screen is just confusing.
**Model images were missing in chat.** Assistant bubbles now show the
avatar of the model that actually wrote the turn -- which is not always
the model the chat is set to now -- falling back to the LLeMbas mark.
The picker shows it too.
**No global or per-model system prompt.** Three layers now: instance
(Admin -> General), model (Admin -> Models), chat. Precedence, not
concatenation: most specific wins outright. Stacking them reads well in
a settings screen and badly in practice, because two layers that
disagree give the model contradictory instructions and nobody can tell
which is losing. The chat panel shows the inherited prompt as
placeholder text so "leave empty to inherit" is not a guess.
**Alignment and button sizing.** Added --control-h and friends to
tokens.css; every button, input and select takes its height from them,
so a mixed row is flush by construction rather than by per-instance
nudging. Icon buttons are square at that height. Added .btn-row,
.card__header/.card__footer and .grid so pages stop carrying inline
styles, and moved every admin page onto them.
**Settings needed structure.** The user settings page is now tabbed
(Account / Models / Appearance / Security) using radio inputs and
sibling selectors -- no JavaScript, and the browser keeps the chosen tab
across a re-render.
Caught while checking: the chat.css surgery had deleted the attachment,
chip and dropzone rules. Restored, and there is now a check that every
literal class used in a template has a CSS rule.
197 tests, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bdce2764b1 |
File attachments: images for vision, PDFs and text into the prompt
Drag, paste or pick a file in the composer. Images go to vision models as multimodal content parts; PDFs and text files have their content extracted and placed in the prompt. Verified end to end against gemma4-e4b-q8 on llama-swap: given a drawing and a text file, it named the red square and blue circle and read the number out of the document. Type is decided by inspecting the bytes, never the filename or the browser's Content-Type -- a .png full of text is stored as text. Images are downscaled to 1400px and re-encoded: a phone photo is several megabytes of base64, which is slow and a large slice of the context window. PDF text is extracted once, at upload, and stored; re-extracting per request would let a reply change because a parser was upgraded. Design points worth keeping: - Images are only sent to models an administrator has marked `vision`. This is not graceful degradation -- most endpoints reject the entire request rather than ignoring an image part. A plain text turn stays a plain string for the same reason: the list form 400s on endpoints that do not implement it. - Images reach the model as base64 data URIs, not links. A local endpoint has no route back to LLeMbas, and a hosted one has no credentials for it. - Non-images are served Content-Disposition: attachment with nosniff, so an uploaded .html can never execute in this origin. Stored names are random; the uploader's name is a label and never a path. - Uploads are unbound until the message is sent, which is what lets a file be removed beforehand. claim() only takes unclaimed rows owned by the sender, so a forged id cannot pull in someone else's file. Abandoned uploads are swept at startup. - A scanned PDF says so rather than silently contributing nothing, and truncation is declared to the model in the document tag so it can admit it did not see page 400. - "Here, look at this" with no words is a legitimate turn, so a message is only empty when it carries neither text nor files. Also fixes auto-titling, which read message["content"] as a string and would have broken on the first multimodal turn. 186 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |