@mongoosejs/studio 0.0.74 → 0.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+ const vm = require('vm');
6
+
7
+ const ExecuteScriptParams = new Archetype({
8
+ userId: {
9
+ $type: mongoose.Types.ObjectId
10
+ },
11
+ chatMessageId: {
12
+ $type: mongoose.Types.ObjectId
13
+ },
14
+ script: {
15
+ $type: String
16
+ }
17
+ }).compile('ExecuteScriptParams');
18
+
19
+ module.exports = ({ db }) => async function executeScript(params) {
20
+ const { userId, chatMessageId, script } = new ExecuteScriptParams(params);
21
+ const ChatMessage = db.model('__Studio_ChatMessage');
22
+
23
+ const chatMessage = await ChatMessage.findById(chatMessageId);
24
+ if (!chatMessage) {
25
+ throw new Error('Chat message not found');
26
+ }
27
+
28
+ // Create a sandbox with the db object
29
+ const logs = [];
30
+ const sandbox = { db, console: {} };
31
+
32
+ // Capture console logs
33
+ sandbox.console.log = function() {
34
+ const args = Array.from(arguments);
35
+ logs.push(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' '));
36
+ };
37
+
38
+ const context = vm.createContext(sandbox);
39
+
40
+ let output;
41
+ let error;
42
+
43
+ try {
44
+ // Execute the script in the sandbox
45
+ output = await vm.runInContext(wrappedScript(script), context);
46
+
47
+ chatMessage.script = script;
48
+ chatMessage.executionResult = { output, logs: logs.join('\n'), error: null };
49
+ await chatMessage.save();
50
+
51
+ return { chatMessage };
52
+ } catch (err) {
53
+ error = err.message;
54
+
55
+ // Update the chat message with the error
56
+ await ChatMessage.updateOne(
57
+ { _id: chatMessageId },
58
+ {
59
+ script,
60
+ executionResult: {
61
+ output: null,
62
+ logs: logs.join('\n'),
63
+ error
64
+ }
65
+ }
66
+ );
67
+
68
+ throw new Error(`Script execution failed: ${error}`);
69
+ }
70
+ };
71
+
72
+ const wrappedScript = script => `(async () => {
73
+ ${script}
74
+ })()`;
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ exports.executeScript = require('./executeScript');
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const getModelDescriptions = require('../../helpers/getModelDescriptions');
5
+ const mongoose = require('mongoose');
6
+
7
+ const CreateChatMessageParams = new Archetype({
8
+ chatThreadId: {
9
+ $type: mongoose.Types.ObjectId
10
+ },
11
+ userId: {
12
+ $type: mongoose.Types.ObjectId
13
+ },
14
+ content: {
15
+ $type: String
16
+ }
17
+ }).compile('CreateChatMessageParams');
18
+
19
+ const systemPrompt = `
20
+ You are a data querying assistant who writes scripts for users accessing MongoDB data using Node.js and Mongoose.
21
+
22
+ Keep scripts concise. Avoid unnecessary comments, error handling, and temporary variables.
23
+
24
+ Do not write any imports or require() statements, that will cause the script to break.
25
+
26
+ Assume the user has pre-defined schemas and models. Do not define any new schemas or models for the user.
27
+
28
+ Use async/await where possible. Assume top-level await is allowed.
29
+
30
+ Think carefully about the user's input and identify the models referred to by the user's query.
31
+
32
+ Format output as Markdown, including code fences for any scripts the user requested.
33
+
34
+ Add a brief text description of what the script does.
35
+
36
+ If the user's query is best answered with a chart, return a Chart.js 4 configuration as \`return { $chart: chartJSConfig };\`. Disable ChartJS animation by default unless user asks for it.
37
+
38
+ Example output:
39
+
40
+ The following script counts the number of users which are not deleted.
41
+
42
+ \`\`\`javascript
43
+ const users = await db.model('User').find({ isDeleted: false });
44
+ return { numUsers: users.length };
45
+ \`\`\`
46
+
47
+ -----------
48
+
49
+ 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.
50
+ `.trim();
51
+
52
+ module.exports = ({ db }) => async function createChatMessage(params) {
53
+ const { chatThreadId, userId, content, script } = new CreateChatMessageParams(params);
54
+ const ChatThread = db.model('__Studio_ChatThread');
55
+ const ChatMessage = db.model('__Studio_ChatMessage');
56
+
57
+ // Check that the user owns the thread
58
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId });
59
+ if (!chatThread) {
60
+ throw new Error('Chat thread not found');
61
+ }
62
+ if (userId != null && chatThread.userId.toString() !== userId.toString()) {
63
+ throw new Error('Not authorized');
64
+ }
65
+
66
+ const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
67
+ const llmMessages = messages.map(m => ({
68
+ role: m.role,
69
+ content: m.content
70
+ }));
71
+ llmMessages.push({ role: 'user', content });
72
+
73
+ if (chatThread.title == null) {
74
+ getChatCompletion([
75
+ { role: 'system', content: 'Summarize the following chat thread in 6 words or less, as a helpful thread title' },
76
+ ...llmMessages
77
+ ]).then(res => {
78
+ const title = res.choices?.[0]?.message?.content;
79
+ chatThread.title = title;
80
+ return chatThread.save();
81
+ }).catch(() => {});
82
+ }
83
+
84
+ llmMessages.unshift({
85
+ role: 'system',
86
+ content: systemPrompt + getModelDescriptions(db)
87
+ });
88
+
89
+ // Create the chat message and get OpenAI response in parallel
90
+ const chatMessages = await Promise.all([
91
+ ChatMessage.create({
92
+ chatThreadId,
93
+ role: 'user',
94
+ content,
95
+ script,
96
+ executionResult: null
97
+ }),
98
+ getChatCompletion(llmMessages).then(res => {
99
+ const content = res.choices?.[0]?.message?.content || '';
100
+ console.log('Content', content, res);
101
+ return ChatMessage.create({
102
+ chatThreadId,
103
+ role: 'assistant',
104
+ content
105
+ });
106
+ })
107
+ ]);
108
+
109
+ return { chatMessages };
110
+ };
111
+
112
+ async function getChatCompletion(messages, options = {}) {
113
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
114
+ method: 'POST',
115
+ headers: {
116
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
117
+ 'Content-Type': 'application/json'
118
+ },
119
+ body: JSON.stringify({
120
+ model: 'gpt-4o',
121
+ max_tokens: 2500,
122
+ ...options,
123
+ messages
124
+ })
125
+ });
126
+
127
+ return await response.json();
128
+ };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const CreateChatThreadParams = new Archetype({
7
+ userId: {
8
+ $type: mongoose.Types.ObjectId
9
+ }
10
+ }).compile('CreateChatThreadParams');
11
+
12
+ module.exports = ({ db }) => async function createChatThread(params) {
13
+ const { userId } = new CreateChatThreadParams(params);
14
+ const ChatThread = db.model('__Studio_ChatThread');
15
+
16
+ const chatThread = await ChatThread.create({ userId });
17
+
18
+ return { chatThread };
19
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const GetChatThreadParams = new Archetype({
7
+ chatThreadId: {
8
+ $type: mongoose.Types.ObjectId
9
+ },
10
+ userId: {
11
+ $type: mongoose.Types.ObjectId
12
+ }
13
+ }).compile('GetChatThreadParams');
14
+
15
+ module.exports = ({ db }) => async function getChatThread(params) {
16
+ const { chatThreadId, userId } = new GetChatThreadParams(params);
17
+ const ChatThread = db.model('__Studio_ChatThread');
18
+ const ChatMessage = db.model('__Studio_ChatMessage');
19
+
20
+ const chatThread = await ChatThread.findById(chatThreadId);
21
+
22
+ if (!chatThread) {
23
+ throw new Error('Chat thread not found');
24
+ }
25
+ if (userId && chatThread.userId.toString() !== userId.toString()) {
26
+ throw new Error('Not authorized');
27
+ }
28
+
29
+ const chatMessages = await ChatMessage.find({ chatThreadId })
30
+ .sort({ createdAt: -1 });
31
+
32
+ return {
33
+ chatThread,
34
+ chatMessages: chatMessages.reverse() // Return in chronological order
35
+ };
36
+ };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ exports.createChatMessage = require('./createChatMessage');
4
+ exports.createChatThread = require('./createChatThread');
5
+ exports.getChatThread = require('./getChatThread');
6
+ exports.listChatThreads = require('./listChatThreads');
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const ListChatThreadsParams = new Archetype({
7
+ userId: {
8
+ $type: mongoose.Types.ObjectId
9
+ }
10
+ }).compile('ListChatThreadsParams');
11
+
12
+ module.exports = ({ db }) => async function listChatThreads(params) {
13
+ // Just validate the params object, but no actual parameters needed
14
+ const { userId } = new ListChatThreadsParams(params);
15
+ const ChatThread = db.model('__Studio_ChatThread');
16
+
17
+ // Get all chat threads
18
+ const chatThreads = await ChatThread.find(userId ? { userId } : {})
19
+ .sort({ updatedAt: -1 }); // Sort by most recently updated
20
+
21
+ return {
22
+ chatThreads
23
+ };
24
+ };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ exports.ChatMessage = require('./ChatMessage');
4
+ exports.ChatThread = require('./ChatThread');
3
5
  exports.Dashboard = require('./Dashboard');
4
6
  exports.Model = require('./Model');
5
7
  exports.Script = require('./Script');
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const mongoose = require('mongoose');
4
+
5
+ const chatMessageSchema = new mongoose.Schema({
6
+ chatThreadId: {
7
+ type: 'ObjectId',
8
+ required: true
9
+ },
10
+ role: {
11
+ type: String,
12
+ enum: ['user', 'assistant'],
13
+ required: true
14
+ },
15
+ content: {
16
+ type: String,
17
+ required: true
18
+ },
19
+ script: {
20
+ type: String,
21
+ required: false
22
+ },
23
+ executionResult: {
24
+ output: mongoose.Schema.Types.Mixed,
25
+ logs: String,
26
+ error: String
27
+ }
28
+ }, { timestamps: true });
29
+
30
+ module.exports = chatMessageSchema;
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const mongoose = require('mongoose');
4
+
5
+ const chatThreadSchema = new mongoose.Schema({
6
+ title: {
7
+ type: String
8
+ },
9
+ userId: {
10
+ type: mongoose.ObjectId,
11
+ ref: 'User'
12
+ }
13
+ }, { timestamps: true });
14
+
15
+ module.exports = chatThreadSchema;
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const listModelPaths = Model => [
4
+ ...Object.entries(Model.schema.paths).map(
5
+ ([path, schemaType]) => `- ${path}: ${schemaType.instance}`
6
+ + (schemaType.options?.ref ? ' (ref: ' + schemaType.options.ref + ')' : '')
7
+ ),
8
+ ...Object.entries(Model.schema.virtuals).filter(([path, virtual]) => virtual.options?.ref).map(
9
+ ([path, virtual]) => `- ${path}: Virtual (ref: ${virtual.options.ref})`
10
+ )
11
+ ].join('\n');
12
+
13
+ const getModelDescriptions = db => Object.values(db.models).filter(Model => !Model.modelName.startsWith('__Studio')).map(Model => `
14
+ ${Model.modelName} (collection: ${Model.collection.collectionName})
15
+ ${listModelPaths(Model)}
16
+ `.trim()).join('\n\n');
17
+
18
+ module.exports = getModelDescriptions;
package/backend/index.js CHANGED
@@ -4,12 +4,19 @@ const Actions = require('./actions');
4
4
  const { applySpec } = require('extrovert');
5
5
  const mongoose = require('mongoose');
6
6
 
7
+ const chatMessageSchema = require('./db/chatMessageSchema');
8
+ const chatThreadSchema = require('./db/chatThreadSchema');
7
9
  const dashboardSchema = require('./db/dashboardSchema');
8
10
 
11
+ const getModelDescriptions = require('./helpers/getModelDescriptions');
12
+
9
13
  module.exports = function backend(db) {
10
14
  db = db || mongoose.connection;
11
-
15
+
12
16
  const Dashboard = db.model('__Studio_Dashboard', dashboardSchema, 'studio__dashboards');
17
+ const ChatMessage = db.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages');
18
+ const ChatThread = db.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads');
19
+
13
20
  const actions = applySpec(Actions, { db });
14
21
  return actions;
15
- };
22
+ };
package/express.js CHANGED
@@ -63,6 +63,7 @@ module.exports = async function(apiUrl, conn, options) {
63
63
  if (!user || !roles) {
64
64
  throw new Error('Not authorized');
65
65
  }
66
+ req.params.initiatedById = user._id;
66
67
  req.params.roles = roles;
67
68
 
68
69
  next();