A folder that carries something, and a way to name one
A folder was a name and nothing else -- and not even that, since PATCH could rename one and nothing in the interface ever called it. It now carries a description, a system prompt, and seeds for the model, the kind and the agent target, with a settings page behind the row. The prompt is a fourth rung on the ladder, chat > folder > model > instance, and it goes above the model deliberately: a model's prompt describes the model wherever it is used, a folder's describes this piece of work whichever model is pointed at it. It is read when a reply is built rather than copied when a chat is made, so editing it reaches the chats already there, and the walk up the parents is bounded and cycle-safe because it runs on the request path. `api/pages.py` mirrors the ladder for the settings panel and had to gain the same rung -- a panel naming the wrong source is worse than one naming none, because it is believed. The seeds fill in what the request left empty and nothing it filled in: the folder says what this work usually needs, the screen in front of somebody says what they want this time. `ssh_profile_id` is a plain string rather than a foreign key, for the reason `compacted_through_id` is, so it is validated on read. Getting *into* a folder needed fixing too. `/api/chats/start` has accepted a folder_id since folders existed and nothing ever sent one, so the only route in was to make the chat elsewhere and move it. There is a New chat here on the row now, and `?folder=` on the new-chat screen. Naming is a themed dialog, and deliberately not htmx's hx-prompt: htmx calls the browser's prompt() synchronously and only then fires htmx:prompt with the answer already in hand, so intercepting the event cannot supply a different one and the grey box appears anyway. `data-prompt` follows the data-confirm-button shape instead -- swallow the click, ask, write the answer into hx-vals, click again behind a guard. JSON.stringify rather than concatenation, or a folder called `"` produces hx-vals that does not parse and the rename silently does nothing. Driven under a DOM stub, and there is a test that no template brings hx-prompt back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -149,23 +149,53 @@ def message_payload(message: Message, *, vision: bool) -> dict[str, Any]:
|
||||
return {"role": message.role, "content": parts}
|
||||
|
||||
|
||||
def folder_system_prompt(db: DBSession, chat: Chat) -> str:
|
||||
"""The nearest prompt on the chat's folder, or on a folder above it.
|
||||
|
||||
Walks up rather than reading one level, because folders nest and a project's
|
||||
prompt belongs on the project rather than on each sub-folder of it. The
|
||||
nearest one wins, which is the same rule the ladder as a whole follows.
|
||||
|
||||
Bounded and cycle-safe the way `api/folders.py:_depth_of` is. Reparenting
|
||||
already refuses to build a cycle, but this runs on the request path for
|
||||
every reply and a row written by something else must not be able to hang it.
|
||||
"""
|
||||
from lembas.db.models import Folder
|
||||
|
||||
folder = chat.folder
|
||||
seen: set[str] = set()
|
||||
while folder is not None and folder.id not in seen:
|
||||
seen.add(folder.id)
|
||||
if (folder.system_prompt or "").strip():
|
||||
return folder.system_prompt.strip()
|
||||
folder = db.get(Folder, folder.parent_id) if folder.parent_id else None
|
||||
return ""
|
||||
|
||||
|
||||
def effective_system_prompt(db: DBSession, chat: Chat) -> str:
|
||||
"""The system prompt a chat actually runs with.
|
||||
|
||||
Three layers, most specific wins outright:
|
||||
Four layers, most specific wins outright:
|
||||
|
||||
chat > model > instance
|
||||
chat > folder > model > instance
|
||||
|
||||
Precedence rather than concatenation. Stacking them reads well in a
|
||||
settings screen and badly in practice: the moment two layers disagree the
|
||||
model gets contradictory instructions and nobody can tell which one is
|
||||
losing. With precedence, "why is it behaving like this" has one answer.
|
||||
|
||||
The folder sits above the model because it is the more specific statement:
|
||||
a model's prompt describes the model wherever it is used, and a folder's
|
||||
describes this piece of work whichever model is pointed at it.
|
||||
"""
|
||||
from lembas.services import settings_store
|
||||
|
||||
if chat.system_prompt.strip():
|
||||
return chat.system_prompt.strip()
|
||||
|
||||
if inherited := folder_system_prompt(db, chat):
|
||||
return inherited
|
||||
|
||||
model = db.scalar(
|
||||
select(Model).where(Model.model_id == chat.model_id).order_by(Model.position)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user