diff --git a/backend/actions/ChatThread/createChatMessage.js b/backend/actions/ChatThread/createChatMessage.js index ffcf7707..3504b96e 100644 --- a/backend/actions/ChatThread/createChatMessage.js +++ b/backend/actions/ChatThread/createChatMessage.js @@ -7,6 +7,8 @@ const agentSystemPrompt = require('../../chatAgent/agentSystemPrompt'); const callLLM = require('../../integrations/callLLM'); const getAgentTools = require('../../chatAgent/getAgentTools'); const getModelDescriptions = require('../../helpers/getModelDescriptions'); +const getModelSkillsMap = require('../../helpers/getModelSkillsMap'); +const formatModelSkillsPrompt = require('../../helpers/formatModelSkillsPrompt'); const mongoose = require('mongoose'); const CreateChatMessageParams = new Archetype({ @@ -81,11 +83,12 @@ module.exports = ({ db, studioConnection, options }) => async function createCha }); } - const modelDescriptions = getModelDescriptions(db); + const modelSkills = await getModelSkillsMap(studioConnection); const system = [ chatThread.agentMode ? agentSystemPrompt : systemPrompt, currentDateTime ? `Current date: ${currentDateTime}` : null, - modelDescriptions, + formatModelSkillsPrompt(modelSkills), + getModelDescriptions(db, modelSkills), options?.context ].filter(Boolean).join('\n\n'); @@ -169,5 +172,5 @@ return { numUsers: users.length }; ----------- -Here is a description of the user's models. Assume these are the only models available in the system unless explicitly instructed otherwise by the user. +Here is a description of the user's models, including any model-specific skills defined by the user. Assume these are the only models available in the system unless explicitly instructed otherwise by the user. Follow model-specific skills exactly when they apply. `.trim(); diff --git a/backend/actions/ChatThread/streamChatMessage.js b/backend/actions/ChatThread/streamChatMessage.js index 3f3c890d..d255510c 100644 --- a/backend/actions/ChatThread/streamChatMessage.js +++ b/backend/actions/ChatThread/streamChatMessage.js @@ -7,6 +7,8 @@ const callLLM = require('../../integrations/callLLM'); const runChatAgent = require('../../chatAgent/runChatAgent'); const streamLLM = require('../../integrations/streamLLM'); const getModelDescriptions = require('../../helpers/getModelDescriptions'); +const getModelSkillsMap = require('../../helpers/getModelSkillsMap'); +const formatModelSkillsPrompt = require('../../helpers/formatModelSkillsPrompt'); const mongoose = require('mongoose'); const CreateChatMessageParams = new Archetype({ @@ -102,15 +104,17 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh script: null, executionResult: null }); + const modelSkills = await getModelSkillsMap(studioConnection); let textStream; try { if (chatThread.agentMode) { - textStream = runChatAgent({ db, llmMessages, currentDateTime, options }); + textStream = runChatAgent({ db, llmMessages, currentDateTime, options, modelSkills }); } else { const system = [ systemPrompt, currentDateTime ? `Current date: ${currentDateTime}` : null, - getModelDescriptions(db), + formatModelSkillsPrompt(modelSkills), + getModelDescriptions(db, modelSkills), options?.context ].filter(Boolean).join('\n\n'); textStream = streamLLM(llmMessages, system, options); @@ -201,5 +205,5 @@ const systemPrompt = ` ----------- - Here is a description of the user's models. Assume these are the only models available in the system unless explicitly instructed otherwise by the user. + Here is a description of the user's models, including any model-specific skills defined by the user. Assume these are the only models available in the system unless explicitly instructed otherwise by the user. Follow model-specific skills exactly when they apply. `.trim(); diff --git a/backend/actions/Model/index.js b/backend/actions/Model/index.js index c090e006..ec06d478 100644 --- a/backend/actions/Model/index.js +++ b/backend/actions/Model/index.js @@ -22,4 +22,5 @@ exports.streamDocumentChanges = require('./streamDocumentChanges'); exports.streamChatMessage = require('./streamChatMessage'); exports.updateDocument = require('./updateDocument'); exports.updateDocuments = require('./updateDocuments'); +exports.updateModelSkill = require('./updateModelSkill'); exports.validateDocument = require('./validateDocument'); diff --git a/backend/actions/Model/listModels.js b/backend/actions/Model/listModels.js index b70c1364..fad90eee 100644 --- a/backend/actions/Model/listModels.js +++ b/backend/actions/Model/listModels.js @@ -11,7 +11,7 @@ const ListModelsParams = new Archetype({ } }).compile('ListModelsParams'); -module.exports = ({ db }) => async function listModels(params) { +module.exports = ({ db, studioConnection }) => async function listModels(params) { const { roles } = new ListModelsParams(params); await authorize('Model.listModels', roles); @@ -49,9 +49,19 @@ module.exports = ({ db }) => async function listModels(params) { removeSpecifiedPaths(schemaPaths, '.$*'); } + const ModelSkill = studioConnection.models['__Studio_ModelSkill']; + const modelSkills = {}; + if (ModelSkill != null) { + const skillDocs = await ModelSkill.find({ modelName: { $in: models } }).lean(); + for (const doc of skillDocs) { + modelSkills[doc.modelName] = doc.skills; + } + } + return { models, modelSchemaPaths, + modelSkills, readyState }; }; diff --git a/backend/actions/Model/updateModelSkill.js b/backend/actions/Model/updateModelSkill.js new file mode 100644 index 00000000..5dc75959 --- /dev/null +++ b/backend/actions/Model/updateModelSkill.js @@ -0,0 +1,37 @@ +'use strict'; + +const Archetype = require('archetype'); +const authorize = require('../../authorize'); + +const UpdateModelSkillParams = new Archetype({ + modelName: { + $type: 'string', + $required: true + }, + skills: { + $type: 'string', + $required: true + }, + roles: { + $type: ['string'] + } +}).compile('UpdateModelSkillParams'); + +module.exports = ({ db, studioConnection }) => async function updateModelSkill(params) { + const { modelName, skills, roles } = new UpdateModelSkillParams(params); + + await authorize('Model.updateModelSkill', roles); + + if (db.models[modelName] == null) { + throw new Error(`Model ${modelName} not found`); + } + + const ModelSkill = studioConnection.model('__Studio_ModelSkill'); + const doc = await ModelSkill.findOneAndUpdate( + { modelName }, + { skills }, + { upsert: true, returnDocument: 'after', sanitizeFilter: true } + ); + + return { doc }; +}; diff --git a/backend/authorize.js b/backend/authorize.js index 600ca601..b5b34cfc 100644 --- a/backend/authorize.js +++ b/backend/authorize.js @@ -32,7 +32,8 @@ const actionsToRequiredRoles = { 'Model.listModels': ['owner', 'admin', 'member', 'readonly'], 'Model.streamDocumentChanges': ['owner', 'admin', 'member', 'readonly'], 'Model.streamChatMessage': ['owner', 'admin', 'member', 'readonly'], - 'Model.updateDocuments': ['owner', 'admin', 'member'] + 'Model.updateDocuments': ['owner', 'admin', 'member'], + 'Model.updateModelSkill': ['owner', 'admin', 'member'] }; module.exports = function authorize(action, roles) { diff --git a/backend/chatAgent/agentSystemPrompt.js b/backend/chatAgent/agentSystemPrompt.js index 41ed7d12..554488f8 100644 --- a/backend/chatAgent/agentSystemPrompt.js +++ b/backend/chatAgent/agentSystemPrompt.js @@ -7,7 +7,7 @@ Your tools are for EXPLORATION ONLY — use them to understand the data before w Always follow this process for each query (do not skip steps): -1. **Identify models**: Based on the user's question and the model descriptions below, identify which models are relevant. +1. **Identify models**: Based on the user's question, the model skills below, and the model descriptions below, identify which models are relevant. 2. **Check document counts**: Use estimatedDocumentCount on each relevant model to understand data volume and choose safe query patterns. 3. **Test assumptions with evidence**: Use find/findOne on each relevant model to verify field names, value shapes, and relationships. Treat every unverified field name, status value, or relationship as unknown until observed. 4. **Draft script**: Write a self-contained script that queries MongoDB directly. Access models via \`db.models.ModelName\` (for example \`db.models.User.findOne(...)\`). Do NOT use \`mongoose.model('Name')\` — schemas are registered on the \`db\` connection, not on the global \`mongoose\` instance — and do NOT use bare \`db.ModelName\` (the model lives under \`db.models\`). @@ -53,5 +53,5 @@ If the user's query is best answered by a table, return an object { $table: { co ----------- -Here is a description of the user's models. Assume these are the only models available in the system unless explicitly instructed otherwise by the user. +Here is a description of the user's models, including any model-specific skills defined by the user. Assume these are the only models available in the system unless explicitly instructed otherwise by the user. Follow model-specific skills exactly when they apply. `.trim(); diff --git a/backend/chatAgent/runChatAgent.js b/backend/chatAgent/runChatAgent.js index 3e30a078..c23c3a71 100644 --- a/backend/chatAgent/runChatAgent.js +++ b/backend/chatAgent/runChatAgent.js @@ -4,12 +4,14 @@ const agentSystemPrompt = require('./agentSystemPrompt'); const getAgentTools = require('./getAgentTools'); const streamLLM = require('../integrations/streamLLM'); const getModelDescriptions = require('../helpers/getModelDescriptions'); +const formatModelSkillsPrompt = require('../helpers/formatModelSkillsPrompt'); -module.exports = function runChatAgent({ db, llmMessages, currentDateTime, options }) { +module.exports = function runChatAgent({ db, llmMessages, currentDateTime, options, modelSkills = {} }) { const system = [ agentSystemPrompt, currentDateTime ? `Current date: ${currentDateTime}` : null, - getModelDescriptions(db), + formatModelSkillsPrompt(modelSkills), + getModelDescriptions(db, modelSkills), options?.context ].filter(Boolean).join('\n\n'); diff --git a/backend/db/modelSkillSchema.js b/backend/db/modelSkillSchema.js new file mode 100644 index 00000000..f873209b --- /dev/null +++ b/backend/db/modelSkillSchema.js @@ -0,0 +1,17 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const modelSkillSchema = new mongoose.Schema({ + modelName: { + type: String, + required: true, + unique: true + }, + skills: { + type: String, + default: '' + } +}); + +module.exports = modelSkillSchema; diff --git a/backend/helpers/formatModelSkillsPrompt.js b/backend/helpers/formatModelSkillsPrompt.js new file mode 100644 index 00000000..83e1e4db --- /dev/null +++ b/backend/helpers/formatModelSkillsPrompt.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = function formatModelSkillsPrompt(modelSkills) { + const entries = Object.entries(modelSkills).filter(([, skills]) => typeof skills === 'string' && skills.trim()); + if (entries.length === 0) { + return null; + } + + return [ + 'The user has defined the following model-specific skills. You MUST follow these instructions when identifying, querying, or writing scripts for each model.', + ...entries.map(([modelName, skills]) => `### ${modelName}\n${skills.trim()}`) + ].join('\n\n'); +}; diff --git a/backend/helpers/getModelDescriptions.js b/backend/helpers/getModelDescriptions.js index a7e067bf..535898cb 100644 --- a/backend/helpers/getModelDescriptions.js +++ b/backend/helpers/getModelDescriptions.js @@ -38,9 +38,13 @@ const listModelPaths = Model => [ ) ].join('\n'); -const getModelDescriptions = db => Object.values(db.models).filter(Model => !Model.modelName.startsWith('__Studio')).map(Model => ` +const getModelDescriptions = (db, modelSkills = {}) => Object.values(db.models).filter(Model => !Model.modelName.startsWith('__Studio')).map(Model => { + const skills = modelSkills[Model.modelName]; + const skillsSection = skills ? `\nSkills: ${skills}` : ''; + return ` ${Model.modelName} (collection: ${Model.collection.collectionName}) -${listModelPaths(Model)} -`.trim()).join('\n\n'); +${listModelPaths(Model)}${skillsSection} +`.trim(); +}).join('\n\n'); module.exports = getModelDescriptions; diff --git a/backend/helpers/getModelSkillsMap.js b/backend/helpers/getModelSkillsMap.js new file mode 100644 index 00000000..5a2819be --- /dev/null +++ b/backend/helpers/getModelSkillsMap.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = async function getModelSkillsMap(studioConnection) { + const ModelSkill = studioConnection?.models?.['__Studio_ModelSkill']; + if (ModelSkill == null) { + return {}; + } + + const docs = await ModelSkill.find({ skills: { $exists: true, $nin: [null, ''] } }).lean(); + return Object.fromEntries(docs.map(doc => [doc.modelName, doc.skills])); +}; diff --git a/backend/index.js b/backend/index.js index 7200677a..1c5e8ece 100644 --- a/backend/index.js +++ b/backend/index.js @@ -8,6 +8,7 @@ const chatMessageSchema = require('./db/chatMessageSchema'); const chatThreadSchema = require('./db/chatThreadSchema'); const dashboardSchema = require('./db/dashboardSchema'); const dashboardResultSchema = require('./db/dashboardResultSchema'); +const modelSkillSchema = require('./db/modelSkillSchema'); module.exports = function backend(db, studioConnection, options) { db = db || mongoose.connection; @@ -20,6 +21,7 @@ module.exports = function backend(db, studioConnection, options) { const DashboardResult = studioConnection.model('__Studio_DashboardResult', dashboardResultSchema, 'studio__dashboardResults'); const ChatMessage = studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages'); const ChatThread = studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads'); + studioConnection.model('__Studio_ModelSkill', modelSkillSchema, 'studio__modelSkills'); let changeStream = null; if (options?.changeStream) { diff --git a/frontend/src/api.js b/frontend/src/api.js index b0e80689..6b2d944d 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -275,6 +275,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) { }, updateDocuments: function updateDocuments(params) { return client.post('', { action: 'Model.updateDocuments', ...params }).then(res => res.data); + }, + updateModelSkill: function updateModelSkill(params) { + return client.post('', { action: 'Model.updateModelSkill', ...params }).then(res => res.data); } }; exports.Task = { @@ -475,6 +478,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) { }, updateDocuments: function updateDocument(params) { return client.post('/Model/updateDocuments', params).then(res => res.data); + }, + updateModelSkill: function updateModelSkill(params) { + return client.post('/Model/updateModelSkill', params).then(res => res.data); } }; exports.Task = { diff --git a/frontend/src/models/models.html b/frontend/src/models/models.html index 34b11201..6f7752c1 100644 --- a/frontend/src/models/models.html +++ b/frontend/src/models/models.html @@ -22,18 +22,31 @@
Recently Viewed
@@ -42,18 +55,31 @@
{{ modelSearch.trim() ? 'Search Results' : 'All Models' }}
@@ -661,6 +687,32 @@

Are you sure?

+ + + app.component('models', { showActionsMenu: false, collectionInfo: null, modelSearch: '', + modelSkills: {}, + shouldShowModelSkillsModal: false, + editingModelSkillsName: null, + editingModelSkills: '', recentlyViewedModels: [], showModelSwitcher: false, showRowNumbers: true, @@ -264,8 +268,9 @@ module.exports = app => app.component('models', { this.query = Object.assign({}, this.$route.query); // Keep UI mode in sync with the URL on remounts. this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1'; - const { models, modelSchemaPaths, readyState } = await api.Model.listModels(); + const { models, modelSkills, modelSchemaPaths, readyState } = await api.Model.listModels(); this.models = models; + this.modelSkills = modelSkills || {}; this.allSchemaPaths = modelSchemaPaths; await this.loadModelCounts(); if (this.currentModel == null && this.models.length > 0) { @@ -475,6 +480,19 @@ module.exports = app => app.component('models', { const after = model.slice(idx + search.length); return `${xss(before)}${xss(match)}${xss(after)}`; }, + openModelSkillsModal(model) { + this.editingModelSkillsName = model; + this.editingModelSkills = this.modelSkills[model] || ''; + this.shouldShowModelSkillsModal = true; + }, + async saveModelSkills() { + const modelName = this.editingModelSkillsName; + const skills = this.editingModelSkills; + await api.Model.updateModelSkill({ modelName, skills }); + this.modelSkills = { ...this.modelSkills, [modelName]: skills }; + this.shouldShowModelSkillsModal = false; + this.$toast.success('Skills saved!'); + }, loadRecentlyViewedModels() { if (typeof window === 'undefined' || !window.localStorage) { return;