Repository navigation
feat: Agent Marketplace categories synced through librechat.yaml
#13309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Educg550
wants to merge
7
commits into
LibreChat-AI:dev
Choose a base branch
from
Educg550:feat/agent-marketplace-categories-yaml-sync
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+492
−3
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
112d2ed
feat: agent marketplace categories synced through librechat.yaml
Educg550 7a3579f
fix: update librechat.example.yaml, internal imports
Educg550 b3db369
fix: guards and YAML formatting for agent-category sync
Educg550 e27e7ad
fix: running db queries system-wide to avoid multi-tenant collision
Educg550 693b905
tests: system run for syncCategories, YAML safe parsing
Educg550 1d86451
docs: update categories example
Educg550 2a620e5
feat: agent category synchronization with localization and configurat…
Educg550 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,48 @@ | ||
| const path = require('path'); | ||
| const { loadYaml, redactConfigSecretMaps, createCustomConfigLoader } = require('@librechat/api'); | ||
| const { logger, runAsSystem } = require('@librechat/data-schemas'); | ||
| const { syncCategories } = require('~/server/utils/agentCategory'); | ||
|
|
||
| const projectRoot = path.resolve(__dirname, '..', '..', '..', '..'); | ||
| const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml'); | ||
|
|
||
| module.exports = createCustomConfigLoader({ | ||
| const loadConfig = createCustomConfigLoader({ | ||
| loadLocal: loadYaml, | ||
| defaultConfigPath, | ||
| redactConfig: redactConfigSecretMaps, | ||
| }); | ||
|
|
||
| async function loadCustomConfig(printConfig = true, options = {}) { | ||
| const customConfig = await loadConfig(printConfig, options); | ||
| if (!customConfig) { | ||
| return customConfig; | ||
| } | ||
|
|
||
| // Injecting custom marketplace categories | ||
| logger.info('Checking for custom marketplace categories to sync.'); | ||
| if (customConfig.interface?.marketplace?.use) { | ||
| logger.info('Marketplace is enabled in config.'); | ||
| const marketplaceConfig = customConfig.interface.marketplace; | ||
| if (marketplaceConfig.categories) { | ||
| logger.info('Marketplace categories configuration found.'); | ||
| // enableDefaultCategories should be set as a boolean, defaulting to true if not specified | ||
| const enableDefaultCategories = | ||
| typeof marketplaceConfig.categories.enableDefaultCategories === 'boolean' | ||
| ? marketplaceConfig.categories.enableDefaultCategories | ||
| : true; | ||
| const customCategoriesList = marketplaceConfig.categories.list; | ||
| if (Array.isArray(customCategoriesList)) { | ||
| logger.info(`Found ${customCategoriesList.length} custom categories to sync.`); | ||
| } else { | ||
| logger.info( | ||
| 'No custom categories `list` provided; only default-category toggling will run.', | ||
| ); | ||
| } | ||
| await runAsSystem(() => syncCategories(customCategoriesList, enableDefaultCategories)); | ||
| } | ||
| } | ||
|
|
||
| return customConfig; | ||
| } | ||
|
|
||
| module.exports = loadCustomConfig; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| const { logger } = require('@librechat/data-schemas'); | ||
|
|
||
| /** Mirrors the hardcoded agent fallback in `data-schemas` (agent schema default and `createAgent`). | ||
| * It must never be deactivated, or agents created without a category become unreachable from | ||
| * every marketplace tab except "All". */ | ||
| const FALLBACK_CATEGORY = 'general'; | ||
|
|
||
| function normalizeValue(raw) { | ||
| return (raw ?? '').toString().trim().toLowerCase(); | ||
| } | ||
|
|
||
| function optionalString(raw) { | ||
| return typeof raw === 'string' ? raw.trim() : undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Synchronizes agent categories with the database. | ||
| * @param {Array|undefined} customCategoriesList - Custom categories from config. When `undefined`, custom categories are left untouched (only defaults are toggled). An explicit empty array opts in to deleting all existing custom categories. | ||
| * @param {boolean} enableDefaultCategories - Whether default categories should be active. | ||
| */ | ||
| async function syncCategories(customCategoriesList, enableDefaultCategories) { | ||
| const { getAllCategories, createCategory, updateCategory, deleteCategory } = require('~/models'); | ||
| logger.info('Syncing custom agent categories...'); | ||
|
|
||
| const dbCategories = await getAllCategories(); | ||
| logger.info(`Found ${dbCategories.length} categories in the database.`); | ||
|
|
||
| logger.info( | ||
| `${enableDefaultCategories ? 'Enabling' : 'Disabling'} default agent categories as per configuration.`, | ||
| ); | ||
| if (!enableDefaultCategories) { | ||
| logger.warn( | ||
| `Keeping the '${FALLBACK_CATEGORY}' category active: it is the fallback assigned to agents created without a category. Declare it in the categories list to change its label or description.`, | ||
| ); | ||
| } | ||
| for (const cat of dbCategories.filter((c) => !c.custom)) { | ||
| const isActive = cat.value === FALLBACK_CATEGORY ? true : enableDefaultCategories; | ||
| logger.info(`${isActive ? 'Enabling' : 'Disabling'} category: ${cat.value}`); | ||
| await updateCategory(cat.value, { isActive }); | ||
| } | ||
|
Educg550 marked this conversation as resolved.
|
||
|
|
||
| if (!Array.isArray(customCategoriesList)) { | ||
| logger.info( | ||
| 'No custom categories list provided; leaving existing custom categories untouched.', | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const defaultCategoriesInDb = dbCategories.filter((cat) => !cat.custom); | ||
| const initialOrder = | ||
| defaultCategoriesInDb.reduce((max, cat) => Math.max(max, cat.order || 0), 0) + 1; | ||
| logger.info(`Current order for new custom categories starts at ${initialOrder}.`); | ||
|
|
||
| const preparedCategories = customCategoriesList.reduce((acc, cat, index) => { | ||
| const normalizedValue = normalizeValue(cat?.value); | ||
| if (!normalizedValue) { | ||
| logger.warn( | ||
| `Skipping invalid custom category entry at index ${index}: missing or empty value.`, | ||
| ); | ||
| return acc; | ||
| } | ||
| acc.push({ | ||
| normalizedValue, | ||
| rawValue: cat.value.toString().trim(), | ||
| label: optionalString(cat.label) || undefined, | ||
| description: optionalString(cat.description), | ||
| order: initialOrder + acc.length, | ||
| }); | ||
| return acc; | ||
| }, []); | ||
|
Educg550 marked this conversation as resolved.
|
||
|
|
||
| const configuredValues = new Set(preparedCategories.map((c) => c.normalizedValue)); | ||
| const categoriesToDelete = dbCategories.filter( | ||
| (cat) => cat.custom && !configuredValues.has(normalizeValue(cat.value)), | ||
| ); | ||
| for (const cat of categoriesToDelete) { | ||
| logger.info(`Deleting custom category not in config: ${cat.value}`); | ||
| await deleteCategory(cat.value); | ||
| } | ||
|
|
||
| for (const prepared of preparedCategories) { | ||
| const existing = dbCategories.find( | ||
| (cat) => normalizeValue(cat.value) === prepared.normalizedValue, | ||
| ); | ||
|
|
||
| /* Default categories keep `custom: false` when overridden, so the deletion pass above (which | ||
| * only targets custom rows) can never remove them, and `ensureDefaultCategories` keeps | ||
| * managing their localization keys. Only explicitly provided fields are applied — otherwise | ||
| * re-declaring a default purely to reactivate it would clobber its `com_` label. */ | ||
| if (existing && !existing.custom) { | ||
| const overrides = { isActive: true }; | ||
| if (prepared.label) { | ||
| overrides.label = prepared.label; | ||
| } | ||
| if (prepared.description !== undefined) { | ||
| overrides.description = prepared.description; | ||
| } | ||
| logger.info(`Overriding default category: ${existing.value}`); | ||
| await updateCategory(existing.value, overrides); | ||
| continue; | ||
| } | ||
|
|
||
| const formattedCategory = { | ||
| value: prepared.normalizedValue, | ||
|
Educg550 marked this conversation as resolved.
|
||
| label: prepared.label || prepared.rawValue, | ||
| description: prepared.description ?? '', | ||
| isActive: true, | ||
| custom: true, | ||
| order: prepared.order, | ||
| }; | ||
|
Educg550 marked this conversation as resolved.
|
||
|
|
||
| if (existing) { | ||
| logger.info(`Updating category: ${formattedCategory.value}`); | ||
| await updateCategory(existing.value, formattedCategory); | ||
| } else { | ||
| logger.info(`Creating category: ${formattedCategory.value}`); | ||
| await createCategory(formattedCategory); | ||
| } | ||
| } | ||
|
|
||
| logger.info('Custom categories synchronized successfully.'); | ||
| } | ||
|
|
||
| module.exports = { | ||
| syncCategories, | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.