diff --git a/api/models/AgentCategory.js b/api/models/AgentCategory.js deleted file mode 100644 index e26e7f3f07..0000000000 --- a/api/models/AgentCategory.js +++ /dev/null @@ -1,121 +0,0 @@ -const mongoose = require('mongoose'); - -/** - * AgentCategory Schema - Dynamic agent category management - * Focused implementation for core features only - */ -const agentCategorySchema = new mongoose.Schema( - { - // Unique identifier for the category (e.g., 'general', 'hr', 'finance') - value: { - type: String, - required: true, - unique: true, - trim: true, - lowercase: true, - index: true, - }, - - // Display label for the category - label: { - type: String, - required: true, - trim: true, - }, - - // Description of the category - description: { - type: String, - trim: true, - default: '', - }, - - // Display order for sorting categories - order: { - type: Number, - default: 0, - index: true, - }, - - // Whether the category is active and should be displayed - isActive: { - type: Boolean, - default: true, - index: true, - }, - }, - { - timestamps: true, - }, -); - -// Indexes for performance -agentCategorySchema.index({ isActive: 1, order: 1 }); - -/** - * Get all active categories sorted by order - * @returns {Promise} Array of active categories - */ -agentCategorySchema.statics.getActiveCategories = function () { - return this.find({ isActive: true }).sort({ order: 1, label: 1 }).lean(); -}; - -/** - * Get categories with agent counts - * @returns {Promise} Categories with agent counts - */ -agentCategorySchema.statics.getCategoriesWithCounts = async function () { - const Agent = mongoose.model('Agent'); - - // Aggregate to get agent counts per category - const categoryCounts = await Agent.aggregate([ - { $match: { category: { $exists: true, $ne: null } } }, - { $group: { _id: '$category', count: { $sum: 1 } } }, - ]); - - // Create a map for quick lookup - const countMap = new Map(categoryCounts.map((c) => [c._id, c.count])); - - // Get all active categories and add counts - const categories = await this.getActiveCategories(); - - return categories.map((category) => ({ - ...category, - agentCount: countMap.get(category.value) || 0, - })); -}; - -/** - * Get valid category values for Agent model validation - * @returns {Promise} Array of valid category values - */ -agentCategorySchema.statics.getValidCategoryValues = function () { - return this.find({ isActive: true }).distinct('value').lean(); -}; - -/** - * Seed initial categories from existing constants - */ -agentCategorySchema.statics.seedCategories = async function (categories) { - const operations = categories.map((category, index) => ({ - updateOne: { - filter: { value: category.value }, - update: { - $setOnInsert: { - value: category.value, - label: category.label || category.value, - description: category.description || '', - order: category.order || index, - isActive: true, - }, - }, - upsert: true, - }, - })); - - return this.bulkWrite(operations); -}; - -const AgentCategory = mongoose.model('AgentCategory', agentCategorySchema); - -module.exports = AgentCategory; diff --git a/api/server/controllers/agents/marketplace.js b/api/server/controllers/agents/marketplace.js index a4d12f3263..a6ae22bc96 100644 --- a/api/server/controllers/agents/marketplace.js +++ b/api/server/controllers/agents/marketplace.js @@ -1,6 +1,6 @@ -const AgentCategory = require('~/models/AgentCategory'); const mongoose = require('mongoose'); const { logger } = require('~/config'); +const { findCategoryByValue, getCategoriesWithCounts } = require('~/models'); // Get the Agent model const Agent = mongoose.model('Agent'); @@ -100,7 +100,7 @@ const getAgentsByCategory = async (req, res) => { const result = await paginateAgents(filter, page, limit); // Get category description from database - const categoryDoc = await AgentCategory.findOne({ value: category, isActive: true }); + const categoryDoc = await findCategoryByValue(category); const categoryInfo = { name: category, description: categoryDoc?.description || '', @@ -183,7 +183,7 @@ const searchAgents = async (req, res) => { const getAgentCategories = async (_req, res) => { try { // Get categories with agent counts from database - const categories = await AgentCategory.getCategoriesWithCounts(); + const categories = await getCategoriesWithCounts(); // Get count of promoted agents for Top Picks const promotedCount = await Agent.countDocuments({ diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index da984b2c3e..cb912d6c9e 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -17,7 +17,7 @@ const { const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants'); const { initializeAzureBlobService } = require('./Files/Azure/initialize'); const { initializeFirebase } = require('./Files/Firebase/initialize'); -const { seedDefaultRoles, initializeRoles } = require('~/models'); +const { seedDefaultRoles, initializeRoles, ensureDefaultCategories } = require('~/models'); const loadCustomConfig = require('./Config/loadCustomConfig'); const handleRateLimits = require('./Config/handleRateLimits'); const { loadDefaultInterface } = require('./start/interface'); @@ -38,6 +38,7 @@ const paths = require('~/config/paths'); const AppService = async (app) => { await initializeRoles(); await seedDefaultRoles(); + await ensureDefaultCategories(); /** @type {TCustomConfig} */ const config = (await loadCustomConfig()) ?? {}; const configDefaults = getConfigDefaults(); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index d0e3dd54ed..ccc2b48c72 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1170,5 +1170,50 @@ "com_agents_no_results": "No agents found. Try another search term.", "com_agents_results_for": "Results for '{{query}}'", "com_nav_agents_marketplace": "Agent Marketplace", - "com_agents_marketplace_subtitle": "Discover and use powerful AI agents to enhance your workflows and productivity" + "com_agents_marketplace_subtitle": "Discover and use powerful AI agents to enhance your workflows and productivity", + "com_ui_copy_url_to_clipboard": "Copy URL to clipboard", + "com_ui_agent_url_copied": "Agent URL copied to clipboard", + "com_ui_search_people_placeholder": "Search for people or groups by name or email", + "com_ui_permission_level": "Permission Level", + "com_ui_grant_access": "Grant Access", + "com_ui_granting": "Granting...", + "com_ui_search_users_groups": "Search Users and Groups", + "com_ui_search_default_placeholder": "Search by name or email (min 2 chars)", + "com_ui_user": "User", + "com_ui_group": "Group", + "com_ui_search_above_to_add": "Search above to add users or groups", + "com_ui_azure_ad": "Entra ID", + "com_ui_remove_user": "Remove {{0}}", + "com_ui_select_options": "Select options...", + "com_ui_no_results_found": "No results found", + "com_ui_try_adjusting_search": "Try adjusting your search terms", + "com_ui_role_viewer": "Viewer", + "com_ui_role_editor": "Editor", + "com_ui_role_manager": "Manager", + "com_ui_role_owner": "Owner", + "com_ui_role_viewer_desc": "Can view and use the agent but cannot modify it", + "com_ui_role_editor_desc": "Can view and modify the agent", + "com_ui_role_manager_desc": "Can view, modify, and delete the agent", + "com_ui_role_owner_desc": "Has full control over the agent including sharing it", + "com_ui_permissions_failed_load": "Failed to load permissions. Please try again.", + "com_ui_permissions_updated_success": "Permissions updated successfully", + "com_ui_permissions_failed_update": "Failed to update permissions. Please try again.", + "com_ui_manage_permissions_for": "Manage Permissions for", + "com_ui_current_access": "Current Access", + "com_ui_no_users_groups_access": "No users or groups have access", + "com_ui_shared_with_count": "Shared with {{0}} {{1}}{{2}}", + "com_ui_person": "person", + "com_ui_people": "people", + "com_ui_and_public": " and public", + "com_ui_revoke_all": "Revoke All", + "com_ui_loading_permissions": "Loading permissions...", + "com_ui_user_group_permissions": "User & Group Permissions", + "com_ui_no_individual_access": "No individual users or groups have access to this agent", + "com_ui_public_access": "Public Access", + "com_ui_saving": "Saving...", + "com_ui_save_changes": "Save Changes", + "com_ui_unsaved_changes": "You have unsaved changes", + "com_ui_share_with_everyone": "Share with everyone", + "com_ui_make_agent_available_all_users": "Make this agent available to all LibreChat users", + "com_ui_public_access_level": "Public access level" } diff --git a/config/seed-categories.js b/config/seed-categories.js deleted file mode 100644 index 2b4f5bba2a..0000000000 --- a/config/seed-categories.js +++ /dev/null @@ -1,106 +0,0 @@ -const connectDb = require('../api/lib/db/connectDb'); -const AgentCategory = require('../api/models/AgentCategory'); - -// Define category constants directly since the constants file was removed -const CATEGORY_VALUES = { - GENERAL: 'general', - HR: 'hr', - RD: 'rd', - FINANCE: 'finance', - IT: 'it', - SALES: 'sales', - AFTERSALES: 'aftersales', -}; - -const CATEGORY_DESCRIPTIONS = { - general: 'General purpose agents for common tasks and inquiries', - hr: 'Agents specialized in HR processes, policies, and employee support', - rd: 'Agents focused on R&D processes, innovation, and technical research', - finance: 'Agents specialized in financial analysis, budgeting, and accounting', - it: 'Agents for IT support, technical troubleshooting, and system administration', - sales: 'Agents focused on sales processes, customer relations, and marketing', - aftersales: 'Agents specialized in post-sale support, maintenance, and customer service', -}; - -/** - * Seed agent categories from existing constants into MongoDB - * This migration creates the initial category data in the database - */ -async function seedCategories() { - try { - await connectDb(); - console.log('Connected to database'); - - // Prepare category data from existing constants - const categoryData = [ - { - value: CATEGORY_VALUES.GENERAL, - label: 'General', - description: CATEGORY_DESCRIPTIONS.general, - order: 0, - }, - { - value: CATEGORY_VALUES.HR, - label: 'Human Resources', - description: CATEGORY_DESCRIPTIONS.hr, - order: 1, - }, - { - value: CATEGORY_VALUES.RD, - label: 'Research & Development', - description: CATEGORY_DESCRIPTIONS.rd, - order: 2, - }, - { - value: CATEGORY_VALUES.FINANCE, - label: 'Finance', - description: CATEGORY_DESCRIPTIONS.finance, - order: 3, - }, - { - value: CATEGORY_VALUES.IT, - label: 'Information Technology', - description: CATEGORY_DESCRIPTIONS.it, - order: 4, - }, - { - value: CATEGORY_VALUES.SALES, - label: 'Sales & Marketing', - description: CATEGORY_DESCRIPTIONS.sales, - order: 5, - }, - { - value: CATEGORY_VALUES.AFTERSALES, - label: 'After Sales', - description: CATEGORY_DESCRIPTIONS.aftersales, - order: 6, - }, - ]; - - console.log('Seeding categories...'); - const result = await AgentCategory.seedCategories(categoryData); - - console.log(`Successfully seeded ${result.upsertedCount} new categories`); - console.log(`Modified ${result.modifiedCount} existing categories`); - - // Verify the seeded data - const categories = await AgentCategory.getActiveCategories(); - console.log('Active categories in database:'); - categories.forEach((cat) => { - console.log(` - ${cat.value}: ${cat.label} (order: ${cat.order})`); - }); - - console.log('Category seeding completed successfully'); - process.exit(0); - } catch (error) { - console.error('Error seeding categories:', error); - process.exit(1); - } -} - -// Run if called directly -if (require.main === module) { - seedCategories(); -} - -module.exports = seedCategories; diff --git a/package.json b/package.json index 73c299297b..ba1c3c4592 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "delete-user": "node config/delete-user.js", "update-banner": "node config/update-banner.js", "delete-banner": "node config/delete-banner.js", - "seed-categories": "node config/seed-categories.js", "backend": "cross-env NODE_ENV=production node api/server/index.js", "backend:dev": "cross-env NODE_ENV=development npx nodemon api/server/index.js", "backend:stop": "node config/stop-backend.js", diff --git a/packages/data-schemas/src/methods/agentCategory.ts b/packages/data-schemas/src/methods/agentCategory.ts index a35e6128f3..ae375811b6 100644 --- a/packages/data-schemas/src/methods/agentCategory.ts +++ b/packages/data-schemas/src/methods/agentCategory.ts @@ -144,6 +144,66 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose')) return await AgentCategory.find({}).sort({ order: 1, label: 1 }).lean(); } + /** + * Ensure default categories exist, seed them if none are present + * @returns Promise - true if categories were seeded, false if they already existed + */ + async function ensureDefaultCategories(): Promise { + const existingCategories = await getAllCategories(); + + if (existingCategories.length > 0) { + return false; // Categories already exist + } + + const defaultCategories = [ + { + value: 'general', + label: 'General', + description: 'General purpose agents for common tasks and inquiries', + order: 0, + }, + { + value: 'hr', + label: 'Human Resources', + description: 'Agents specialized in HR processes, policies, and employee support', + order: 1, + }, + { + value: 'rd', + label: 'Research & Development', + description: 'Agents focused on R&D processes, innovation, and technical research', + order: 2, + }, + { + value: 'finance', + label: 'Finance', + description: 'Agents specialized in financial analysis, budgeting, and accounting', + order: 3, + }, + { + value: 'it', + label: 'Information Technology', + description: 'Agents for IT support, technical troubleshooting, and system administration', + order: 4, + }, + { + value: 'sales', + label: 'Sales & Marketing', + description: 'Agents focused on sales processes, customer relations, and marketing', + order: 5, + }, + { + value: 'aftersales', + label: 'After Sales', + description: 'Agents specialized in post-sale support, maintenance, and customer service', + order: 6, + }, + ]; + + await seedCategories(defaultCategories); + return true; // Categories were seeded + } + return { getActiveCategories, getCategoriesWithCounts, @@ -155,6 +215,7 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose')) deleteCategory, findCategoryById, getAllCategories, + ensureDefaultCategories, }; } diff --git a/packages/data-schemas/src/models/agentCategory.ts b/packages/data-schemas/src/models/agentCategory.ts new file mode 100644 index 0000000000..1ba26c037a --- /dev/null +++ b/packages/data-schemas/src/models/agentCategory.ts @@ -0,0 +1,9 @@ +import agentCategorySchema from '~/schema/agentCategory'; +import type * as t from '~/types'; + +/** + * Creates or returns the AgentCategory model using the provided mongoose instance and schema + */ +export function createAgentCategoryModel(mongoose: typeof import('mongoose')) { + return mongoose.models.AgentCategory || mongoose.model('AgentCategory', agentCategorySchema); +} \ No newline at end of file diff --git a/packages/data-schemas/src/models/index.ts b/packages/data-schemas/src/models/index.ts index bf8776b60e..dd1d8ee23c 100644 --- a/packages/data-schemas/src/models/index.ts +++ b/packages/data-schemas/src/models/index.ts @@ -5,6 +5,7 @@ import { createBalanceModel } from './balance'; import { createConversationModel } from './convo'; import { createMessageModel } from './message'; import { createAgentModel } from './agent'; +import { createAgentCategoryModel } from './agentCategory'; import { createRoleModel } from './role'; import { createActionModel } from './action'; import { createAssistantModel } from './assistant'; @@ -37,6 +38,7 @@ export function createModels(mongoose: typeof import('mongoose')) { Conversation: createConversationModel(mongoose), Message: createMessageModel(mongoose), Agent: createAgentModel(mongoose), + AgentCategory: createAgentCategoryModel(mongoose), Role: createRoleModel(mongoose), Action: createActionModel(mongoose), Assistant: createAssistantModel(mongoose), diff --git a/packages/data-schemas/src/schema/agentCategory.ts b/packages/data-schemas/src/schema/agentCategory.ts index 4bc0d778dd..61792de3f8 100644 --- a/packages/data-schemas/src/schema/agentCategory.ts +++ b/packages/data-schemas/src/schema/agentCategory.ts @@ -1,12 +1,5 @@ import { Schema, Document } from 'mongoose'; - -export interface IAgentCategory extends Document { - value: string; - label: string; - description?: string; - order: number; - isActive: boolean; -} +import type { IAgentCategory } from '~/types'; const agentCategorySchema = new Schema( { @@ -46,4 +39,4 @@ const agentCategorySchema = new Schema( agentCategorySchema.index({ isActive: 1, order: 1 }); -export default agentCategorySchema; \ No newline at end of file +export default agentCategorySchema; diff --git a/packages/data-schemas/src/types/index.ts b/packages/data-schemas/src/types/index.ts index 3e6539492b..679553aa82 100644 --- a/packages/data-schemas/src/types/index.ts +++ b/packages/data-schemas/src/types/index.ts @@ -9,6 +9,7 @@ export * from './balance'; export * from './banner'; export * from './message'; export * from './agent'; +export * from './agentCategory'; export * from './role'; export * from './action'; export * from './assistant';