Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions backend/actions/ChatThread/createChatMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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();
10 changes: 7 additions & 3 deletions backend/actions/ChatThread/streamChatMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
1 change: 1 addition & 0 deletions backend/actions/Model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
12 changes: 11 additions & 1 deletion backend/actions/Model/listModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
};
};
37 changes: 37 additions & 0 deletions backend/actions/Model/updateModelSkill.js
Original file line number Diff line number Diff line change
@@ -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 };
};
3 changes: 2 additions & 1 deletion backend/authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions backend/chatAgent/agentSystemPrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\`).
Expand Down Expand Up @@ -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();
6 changes: 4 additions & 2 deletions backend/chatAgent/runChatAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
17 changes: 17 additions & 0 deletions backend/db/modelSkillSchema.js
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions backend/helpers/formatModelSkillsPrompt.js
Original file line number Diff line number Diff line change
@@ -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');
};
10 changes: 7 additions & 3 deletions backend/helpers/getModelDescriptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
11 changes: 11 additions & 0 deletions backend/helpers/getModelSkillsMap.js
Original file line number Diff line number Diff line change
@@ -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]));
};
2 changes: 2 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down
80 changes: 66 additions & 14 deletions frontend/src/models/models.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,31 @@
<div class="px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">Recently Viewed</div>
<ul role="list">
<li v-for="model in filteredRecentModels" :key="'recent-' + model">
<router-link
:to="'/model/' + model"
class="flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary"
:class="model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'">
<span class="truncate" v-html="highlightMatch(model)"></span>
<div
class="group flex items-center gap-1 rounded-md"
:class="model === currentModel ? 'bg-gray-200' : 'hover:bg-muted'">
<router-link
:to="'/model/' + model"
class="flex flex-1 items-center min-w-0 py-1.5 pl-2 pr-1 text-sm text-content-secondary"
:class="model === currentModel ? 'font-semibold text-content' : ''">
<span class="truncate" v-html="highlightMatch(model)"></span>
</router-link>
<button
type="button"
@click.stop="openModelSkillsModal(model)"
class="shrink-0 rounded p-1 text-gray-400 hover:bg-surface hover:text-content-secondary"
title="Edit model skills">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-3.5 w-3.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
<span
v-if="modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel"
class="ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]"
class="shrink-0 mr-1 text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]"
>
{{formatCompactCount(modelDocumentCounts[model])}}
</span>
</router-link>
</div>
</li>
</ul>
<div class="border-b border-edge my-2"></div>
Expand All @@ -42,18 +55,31 @@
<div class="px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ modelSearch.trim() ? 'Search Results' : 'All Models' }}</div>
<ul role="list">
<li v-for="model in filteredModels" :key="'all-' + model">
<router-link
:to="'/model/' + model"
class="flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary"
:class="model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'">
<span class="truncate" v-html="highlightMatch(model)"></span>
<div
class="group flex items-center gap-1 rounded-md"
:class="model === currentModel ? 'bg-gray-200' : 'hover:bg-muted'">
<router-link
:to="'/model/' + model"
class="flex flex-1 items-center min-w-0 py-1.5 pl-2 pr-1 text-sm text-content-secondary"
:class="model === currentModel ? 'font-semibold text-content' : ''">
<span class="truncate" v-html="highlightMatch(model)"></span>
</router-link>
<button
type="button"
@click.stop="openModelSkillsModal(model)"
class="shrink-0 rounded p-1 text-gray-400 hover:bg-surface hover:text-content-secondary"
title="Edit model skills">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-3.5 w-3.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
<span
v-if="modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel"
class="ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]"
class="shrink-0 mr-1 text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]"
>
{{formatCompactCount(modelDocumentCounts[model])}}
</span>
</router-link>
</div>
</li>
</ul>
<div v-if="filteredModels.length === 0 && modelSearch.trim()" class="px-2 py-2 text-sm text-content-tertiary">
Expand Down Expand Up @@ -661,6 +687,32 @@ <h2 class="text-xl font-bold mb-3">Are you sure?</h2>
</div>
</template>
</modal>
<modal v-if="shouldShowModelSkillsModal">
<template v-slot:body>
<div class="modal-exit" @click="shouldShowModelSkillsModal = false">&times;</div>
<div class="text-xl font-bold mb-2">Skills for {{ editingModelSkillsName }}</div>
<p class="text-sm text-content-tertiary mb-3">These skills are included as context when using the Chat tab.</p>
<textarea
v-model="editingModelSkills"
rows="8"
placeholder="Add skills for this model..."
class="w-full rounded-md border border-edge bg-surface px-3 py-2 text-sm text-content placeholder:text-gray-400 focus:border-edge-strong focus:outline-none focus:ring-1 focus:ring-gray-300"
></textarea>
<div class="flex gap-4 mt-4">
<async-button
@click="saveModelSkills"
class="rounded bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
Save
</async-button>
<button
type="button"
@click="shouldShowModelSkillsModal = false"
class="rounded bg-gray-400 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0">
Cancel
</button>
</div>
</template>
</modal>
<model-switcher
:show="showModelSwitcher"
:models="models"
Expand Down
Loading
Loading