🏷️ feat: Configurable and User-Created Prompt Categories - #16574
TomasPalsson wants to merge 16 commits into
Conversation
…m in startup config
…refresh categories after save
…nless custom categories are allowed
|
Hi Tomas, I would like to review/merge some of your PRs soon |
|
thanks for your patience, getting the dev branch up to date with our experimental changes first |
There was a problem hiding this comment.
Reviewed head ab7d8aa40c74e4af0cf38d5c430d25db73fb0c61. One P1 and four P2 findings below.
Source-level reproductions used the frozen-head functions with dependency boundaries stubbed. Local Jest and TypeScript checks were blocked by missing dependencies. CI tests, typechecks, builds, integration checks, Lighthouse, and runtime smoke passed; Static checks failed on class ordering in CategorySelector.tsx.
| const { accessibleIds } = await deps.getPromptGroupAccessContext({ | ||
| userId: req.user?.id ?? '', | ||
| role: req.user?.role, | ||
| }); | ||
| customValues = await deps.getDistinctPromptGroupCategories(accessibleIds); | ||
| } | ||
| res.status(200).send(resolvePromptCategories(config, customValues)); |
There was a problem hiding this comment.
[P1] Apply current prompt-content policy to stored category discovery
With allowCustom: true, this exposes stored category text without consulting req.config.filters. category is a protected prompt field, and existing prompt reads reapply the current policy and suppress blocked group metadata. An older category matching a newly enabled filters.prompts.pii rule can therefore be hidden by prompt reads yet returned verbatim by /api/categories as both value and label. Apply the same content-policy boundary before exposing stored categories, and cover a category stored before the blocking policy was enabled.
| requireJwtAuth, | ||
| configMiddleware, | ||
| createGetPromptCategoriesHandler({ | ||
| getPromptGroupAccessContext, | ||
| getDistinctPromptGroupCategories, |
There was a problem hiding this comment.
[P2] Enforce prompt-use permission before exposing stored categories
This route authenticates the user but does not enforce PROMPTS.USE, unlike /api/prompts. getPromptGroupAccessContext resolves resource ACLs independently of that role permission. With custom categories enabled, a user denied access to the Prompt Library can still retrieve stored category names through retained VIEW ACLs or public grants. Gate stored-category discovery on the existing prompt-use permission check. Configured categories can remain available to shared consumers such as Skills.
| const seen = new Set(result.flatMap((c) => [normalize(c.value), normalize(c.label)])); | ||
| for (const value of customValues) { | ||
| const key = normalize(value); | ||
| if (seen.has(key)) { | ||
| continue; |
There was a problem hiding this comment.
[P2] Keep category deduplication consistent with exact-match filtering
Create/update preserve case, and buildPromptGroupFilter uses exact category values. If two authors independently create Alpha and alpha before sharing their prompts, this keeps only one option; selecting it excludes prompts using the other spelling. Label-based deduplication also hides an existing People category when config contains { value: "hr", label: "People" }, but the remaining option filters for hr. Retain selectable stored identities or make filtering honor the same equivalence rules, without rewriting stored data.
| const fromEntry = ({ value, label, icon, color }: CategoryEntry): TCategory => ({ | ||
| value, | ||
| label: label ?? value, |
There was a problem hiding this comment.
[P2] Apply schema trimming to runtime configured category values
The config loader validates with configSchema but returns the original object, and AppService copies config.prompts unchanged. A valid YAML entry with value: " hr " therefore reaches this function untrimmed. Selecting it saves hr because create/update validation trims categories, while the sidebar filters for " hr " and misses that prompt. When custom categories are enabled, normalized deduplication also suppresses the stored hr option. Apply schema transformations to runtime category entries, including overrides, before returning their values.
| ); | ||
|
|
||
| addGroupToAll(queryClient, group); | ||
| queryClient.invalidateQueries([QueryKeys.categories]); |
There was a problem hiding this comment.
[P2] Invalidate categories on prompt-group deletion too
Now that categories are derived from stored groups, create/update invalidation alone is insufficient. Neither useDeletePromptGroup nor the response.promptGroup branch of useDeletePrompt invalidates this query. Deleting the only prompt using Onboarding, including deleting its final version and therefore its group, leaves Onboarding in the selector and sidebar filter. useGetCategories disables refetch on mount, focus, and reconnect, so it remains stale until another invalidating mutation or a reload. Invalidate categories on both group-deletion paths and cover deletion of the last categorized prompt.
Summary
The Prompt Library offers a fixed list of nine categories (Ideas, Travel, Learning, Writing, Shopping, Code, Misc., Roleplay, Finance) that lives in server code, so an organisation that wants categories matching its departments has to fork, and prompt authors cannot tag a prompt with their own word even though the stored
categoryfield is already free text. Related to #3353.This adds a
prompts.categoriessection tolibrechat.yaml, reusing the vocabulary of the agent-category proposal (#13309):enableDefaultCategorieshides the built-ins,listadds entries (value, optionallabelthat is either acom_translation key or literal text, optionaliconfrom the icons the Prompt Library already uses, optionalcolorfromseries-1…series-8), and an entry whose value matches a built-in overrides it in place.allowCustom(defaultfalse) lets authors create a category from the prompt category selector and makes the list also carry the distinct custom values stored on prompts the requester can view. With nopromptssection the endpoint returns exactly today's nine, in today's order.Categories stay stateless: the list is resolved per request from config plus stored values, so there is no new collection or sync job, per-role/group/tenant admin overrides apply through the existing override merge, and removing a config entry only hides it. Prompt create and update now reject a category over 100 characters, starting with the reserved
sys__prefix, or containing control characters, with the existing 400 validation shape. Docs: LibreChat-AI/docs#793.How it works
Resolution order: built-ins in today's order (unless disabled) with matching list entries merged in place, then remaining list entries in yaml order, then custom values not already present (case-insensitive on value and label).
/api/configexposespromptCategories.allowCustomso the client knows whether to offer "New category…".Type of change
Testing
Tested environments/configuration: production build (
npm run build:client,node api/server/index.js), Chromium via Playwright, MongoDB 7, light and dark theme. Config A:enableDefaultCategories: false,allowCustom: true, entrieshr(Human Resources, users, series-7),legal(Legal, beaker, series-3),code(Engineering). Config B: nopromptssection.sys__xdisables the button with "Category names cannot start with sys__".Automated tests:
packages/data-provider/specs/config.spec.ts,packages/api/src/prompts/categories.spec.ts,packages/api/src/prompts/schemas.spec.ts,packages/data-schemas/src/methods/prompt.spec.ts("distinct categories", in-memory Mongo),packages/data-schemas/src/app/service.spec.ts,api/server/routes/categories.test.js(defaults unchanged, role override through the real override merge, no error leak, access scoped between two users, no reads when off),api/server/routes/prompts.test.js(category validation), clientuseCategories,CategoryIcon,CategorySelector(prompt and skill),FilterPrompts.npm run static-checks:full -- --against upstream/devandnpm run lighthouse(median LCP 3476 ms) pass.Screenshots / recordings
The selector's "New category…" item and inline form are a new surface, so they have no "before". With no config the lists are unchanged from
dev.Screenshots (Config A in light and dark, the keyboard create flow, the filter after save, and Config B matching
dev) are added below in a comment.Risk / compatibility
{ label, value }objects as before, andallowCustomdefaults tofalse, so no extra database reads.allowCustomdefaults tofalseper the "new levers reproduce today's behaviour" rule. Iftruefits Enhancement: [Prompt Library] Let users create their own categories #3353 better, it is a one-line flip.labelshows the raw value rather than the built-in's translated label (a missing label renders the value). The 200 custom-value cap applies after a case-insensitive sort over the prompts the user can view, so if a user can see more than 200 distinct custom values, the ones late in the alphabet are left out. A whitespace-only category on update now returns 400, while''still clears it.CategoryIconreads the cached categories query withoutuseCategories' access gate, because it only renders inside the Prompts UI.