@mongoosejs/studio 0.1.6 → 0.1.8

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const callLLM = require('../../integrations/callLLM');
5
6
  const getModelDescriptions = require('../../helpers/getModelDescriptions');
6
7
  const mongoose = require('mongoose');
7
8
 
@@ -13,7 +14,7 @@ const CreateChatMessageParams = new Archetype({
13
14
  $type: mongoose.Types.ObjectId
14
15
  },
15
16
  content: {
16
- $type: String
17
+ $type: 'string'
17
18
  },
18
19
  authorization: {
19
20
  $type: 'string'
@@ -42,29 +43,43 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
42
43
  const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
43
44
  const llmMessages = messages.map(m => ({
44
45
  role: m.role,
45
- content: m.content
46
+ content: [{
47
+ type: 'text',
48
+ text: m.content
49
+ }]
46
50
  }));
47
- llmMessages.push({ role: 'user', content });
51
+ llmMessages.push({ role: 'user', content: [{ type: 'text', text: content }] });
48
52
 
49
53
  let summarizePromise = Promise.resolve();
50
54
  if (chatThread.title == null) {
51
- summarizePromise = summarizeChatThread(llmMessages, authorization, options).then(res => {
52
- const title = res.response;
55
+ const threadText = messages
56
+ .filter(m => m.role === 'user' || m.role === 'assistant')
57
+ .map(m => `${m.role.toUpperCase()}: ${m.content}`)
58
+ .join('\n')
59
+ .slice(0, 5000);
60
+ summarizePromise = callLLM(
61
+ [{
62
+ role: 'user',
63
+ content: [{
64
+ type: 'text',
65
+ text: 'Summarize the following chat thread into a concise, helpful title (≤ 6 words).\n\n' +
66
+ `${threadText}\n\n` +
67
+ 'Return only the title.'
68
+ }]
69
+ }],
70
+ 'You are a helpful assistant that summarizes chat threads into titles.',
71
+ options
72
+ ).then(res => {
73
+ const title = res.text;
53
74
  chatThread.title = title;
54
75
  return chatThread.save();
55
76
  });
56
77
  }
57
78
 
58
- if (options?.context) {
59
- llmMessages.unshift({
60
- role: 'system',
61
- content: options.context
62
- });
63
- }
64
-
65
79
  const modelDescriptions = getModelDescriptions(db);
80
+ const system = systemPrompt + '\n\n' + modelDescriptions + (options?.context ? '\n\n' + options.context : '');
66
81
 
67
- // Create the chat message and get OpenAI response in parallel
82
+ // Create the chat message and get LLM response in parallel
68
83
  const chatMessages = await Promise.all([
69
84
  ChatMessage.create({
70
85
  chatThreadId,
@@ -73,8 +88,8 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
73
88
  script,
74
89
  executionResult: null
75
90
  }),
76
- createChatMessageCore(llmMessages, modelDescriptions, options?.model, authorization, options).then(res => {
77
- const content = res.response;
91
+ callLLM(llmMessages, system, options).then(res => {
92
+ const content = res.text;
78
93
  return ChatMessage.create({
79
94
  chatThreadId,
80
95
  role: 'assistant',
@@ -87,157 +102,43 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
87
102
  return { chatMessages, chatThread };
88
103
  };
89
104
 
90
- async function summarizeChatThread(messages, authorization, options) {
91
- if (options?.openAIAPIKey) {
92
- const response = await callOpenAI({
93
- apiKey: options.openAIAPIKey,
94
- model: options.model,
95
- messages: [
96
- {
97
- role: 'system',
98
- content: 'Summarize the following conversation into a concise title of at most 7 words. Respond with the title only.'
99
- },
100
- ...messages
101
- ]
102
- });
103
-
104
- return { response };
105
- }
106
-
107
- const headers = { 'Content-Type': 'application/json' };
108
- if (authorization) {
109
- headers.Authorization = authorization;
110
- }
111
- const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/summarizeChatThread', {
112
- method: 'POST',
113
- headers,
114
- body: JSON.stringify({
115
- messages
116
- })
117
- }).then(response => {
118
- if (response.status < 200 || response.status >= 400) {
119
- return response.json().then(data => {
120
- throw new Error(`Mongoose Studio chat thread summarization error: ${data.message}`);
121
- });
122
- }
123
- return response;
124
- });
125
-
126
- return await response.json();
127
- }
128
-
129
- async function createChatMessageCore(messages, modelDescriptions, model, authorization, options) {
130
- if (options?.openAIAPIKey) {
131
- const openAIMessages = [];
132
- if (modelDescriptions) {
133
- openAIMessages.push({
134
- role: 'system',
135
- content: `${systemPrompt}:\n\n${modelDescriptions}`
136
- });
137
- }
138
- openAIMessages.push(...messages);
139
-
140
- const response = await callOpenAI({
141
- apiKey: options.openAIAPIKey,
142
- model,
143
- messages: openAIMessages
144
- });
145
-
146
- return { response };
147
- }
148
-
149
- const headers = { 'Content-Type': 'application/json' };
150
- if (authorization) {
151
- headers.Authorization = authorization;
152
- }
153
- const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/createChatMessage', {
154
- method: 'POST',
155
- headers,
156
- body: JSON.stringify({
157
- messages,
158
- modelDescriptions,
159
- model
160
- })
161
- }).then(response => {
162
- if (response.status < 200 || response.status >= 400) {
163
- return response.json().then(data => {
164
- throw new Error(`Mongoose Studio chat completion error: ${data.message}`);
165
- });
166
- }
167
- return response;
168
- });
169
-
170
- return await response.json();
171
- }
172
-
173
105
  const systemPrompt = `
174
- You are a data querying assistant who writes scripts for users accessing MongoDB data using Node.js and Mongoose.
175
-
176
- Keep scripts concise. Avoid unnecessary comments, error handling, and temporary variables.
177
-
178
- Do not write any imports or require() statements, that will cause the script to break.
106
+ You are a data querying assistant who writes scripts for users accessing MongoDB data using Node.js and Mongoose.
179
107
 
180
- If the user approves the script, the script will run in the Node.js server in a sandboxed vm.createContext() call with only 1 global variable: db, which contains the Mongoose connection. The script return value will then send the response via JSON to the client. Be aware that the result of the query will be serialized to JSON before being displayed to the user. MAKE SURE TO RETURN A VALUE FROM THE SCRIPT.
108
+ Keep scripts concise. Avoid unnecessary comments, error handling, and temporary variables.
181
109
 
182
- Optimize scripts for readability first, followed by reliability, followed by performance. Avoid using the aggregation framework unless explicitly requested by the user. Use indexed fields in queries where possible.
110
+ Do not write any imports or require() statements, that will cause the script to break.
183
111
 
184
- Assume the user has pre-defined schemas and models. Do not define any new schemas or models for the user.
112
+ If the user approves the script, the script will run in the Node.js server in a sandboxed vm.createContext() call with only 1 global variable: db, which contains the Mongoose connection. The script return value will then send the response via JSON to the client. Be aware that the result of the query will be serialized to JSON before being displayed to the user. MAKE SURE TO RETURN A VALUE FROM THE SCRIPT.
185
113
 
186
- Use async/await where possible. Assume top-level await is allowed.
114
+ Optimize scripts for readability first, followed by reliability, followed by performance. Avoid using the aggregation framework unless explicitly requested by the user. Use indexed fields in queries where possible.
187
115
 
188
- Write at most one script, unless the user explicitly asks for multiple scripts.
116
+ Assume the user has pre-defined schemas and models. Do not define any new schemas or models for the user.
189
117
 
190
- Think carefully about the user's input and identify the models referred to by the user's query.
118
+ Use async/await where possible. Assume top-level await is allowed.
191
119
 
192
- Format output as Markdown, including code fences for any scripts the user requested.
120
+ Write at most one script, unless the user explicitly asks for multiple scripts.
193
121
 
194
- Add a brief text description of what the script does.
122
+ Think carefully about the user's input and identify the models referred to by the user's query.
195
123
 
196
- 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. Set responsive: true, maintainAspectRatio: false options unless the user explicitly asks.
124
+ Format output as Markdown, including code fences for any scripts the user requested.
197
125
 
198
- If the user\'s query is best answered by a map, return an object { $featureCollection } which contains a GeoJSON FeatureCollection
126
+ Add a brief text description of what the script does.
199
127
 
200
- Example output:
128
+ 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. Set responsive: true, maintainAspectRatio: false options unless the user explicitly asks.
201
129
 
202
- The following script counts the number of users which are not deleted.
130
+ If the user\'s query is best answered by a map, return an object { $featureCollection } which contains a GeoJSON FeatureCollection
203
131
 
204
- \`\`\`javascript
205
- const users = await db.model('User').find({ isDeleted: false });
206
- return { numUsers: users.length };
207
- \`\`\`
132
+ Example output:
208
133
 
209
- -----------
134
+ The following script counts the number of users which are not deleted.
210
135
 
211
- 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.
212
- `.trim();
136
+ \`\`\`javascript
137
+ const users = await db.model('User').find({ isDeleted: false });
138
+ return { numUsers: users.length };
139
+ \`\`\`
213
140
 
214
- async function callOpenAI({ apiKey, model, messages }) {
215
- if (!apiKey) {
216
- throw new Error('OpenAI API key required');
217
- }
218
-
219
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
220
- method: 'POST',
221
- headers: {
222
- 'Content-Type': 'application/json',
223
- Authorization: `Bearer ${apiKey}`
224
- },
225
- body: JSON.stringify({
226
- model: model || 'gpt-4o-mini',
227
- messages
228
- })
229
- });
230
-
231
- const data = await response.json();
232
-
233
- if (response.status < 200 || response.status >= 400) {
234
- throw new Error(`OpenAI chat completion error ${response.status}: ${data.error?.message || data.message || 'Unknown error'}`);
235
- }
236
-
237
- const content = data?.choices?.[0]?.message?.content;
238
- if (!content) {
239
- throw new Error('OpenAI chat completion error: missing response content');
240
- }
141
+ -----------
241
142
 
242
- return content.trim();
243
- }
143
+ 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.
144
+ `.trim();
@@ -73,13 +73,26 @@ module.exports = ({ db }) => async function getDocuments(params) {
73
73
 
74
74
  const schemaPaths = {};
75
75
  for (const path of Object.keys(Model.schema.paths)) {
76
+ const schemaType = Model.schema.paths[path];
76
77
  schemaPaths[path] = {
77
- instance: Model.schema.paths[path].instance,
78
+ instance: schemaType.instance,
78
79
  path,
79
- ref: Model.schema.paths[path].options?.ref,
80
- required: Model.schema.paths[path].options?.required,
81
- enum: Model.schema.paths[path].options?.enum
80
+ ref: schemaType.options?.ref,
81
+ required: schemaType.options?.required,
82
+ enum: schemaType.options?.enum
82
83
  };
84
+ if (schemaType.schema) {
85
+ schemaPaths[path].schema = {};
86
+ for (const subpath of Object.keys(schemaType.schema.paths)) {
87
+ schemaPaths[path].schema[subpath] = {
88
+ instance: schemaType.schema.paths[subpath].instance,
89
+ path: subpath,
90
+ ref: schemaType.schema.paths[subpath].options?.ref,
91
+ required: schemaType.schema.paths[subpath].options?.required,
92
+ enum: schemaType.schema.paths[subpath].options?.enum
93
+ };
94
+ }
95
+ }
83
96
  }
84
97
  removeSpecifiedPaths(schemaPaths, '.$*');
85
98
 
@@ -63,13 +63,26 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
63
63
 
64
64
  const schemaPaths = {};
65
65
  for (const path of Object.keys(Model.schema.paths)) {
66
+ const schemaType = Model.schema.paths[path];
66
67
  schemaPaths[path] = {
67
- instance: Model.schema.paths[path].instance,
68
+ instance: schemaType.instance,
68
69
  path,
69
- ref: Model.schema.paths[path].options?.ref,
70
- required: Model.schema.paths[path].options?.required,
71
- enum: Model.schema.paths[path].options?.enum
70
+ ref: schemaType.options?.ref,
71
+ required: schemaType.options?.required,
72
+ enum: schemaType.options?.enum
72
73
  };
74
+ if (schemaType.schema) {
75
+ schemaPaths[path].schema = {};
76
+ for (const subpath of Object.keys(schemaType.schema.paths)) {
77
+ schemaPaths[path].schema[subpath] = {
78
+ instance: schemaType.schema.paths[subpath].instance,
79
+ path: subpath,
80
+ ref: schemaType.schema.paths[subpath].options?.ref,
81
+ required: schemaType.schema.paths[subpath].options?.required,
82
+ enum: schemaType.schema.paths[subpath].options?.enum
83
+ };
84
+ }
85
+ }
73
86
  }
74
87
  removeSpecifiedPaths(schemaPaths, '.$*');
75
88
 
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const { createAnthropic } = require('@ai-sdk/anthropic');
4
+ const { createOpenAI } = require('@ai-sdk/openai');
5
+ const { generateText } = require('ai');
6
+
7
+ module.exports = async function callLLM(messages, system, options) {
8
+ let provider = null;
9
+ let model = null;
10
+ if (options?.openAIAPIKey && options?.anthropicAPIKey) {
11
+ throw new Error('Cannot set both OpenAI and Anthropic API keys');
12
+ }
13
+
14
+ if (options?.openAIAPIKey) {
15
+ provider = createOpenAI({ apiKey: options.openAIAPIKey });
16
+ model = options?.model ?? 'gpt-4o-mini';
17
+ } else if (options?.anthropicAPIKey) {
18
+ provider = createAnthropic({ apiKey: options.anthropicAPIKey });
19
+ model = options?.model ?? 'claude-haiku-4-5-20251001';
20
+ }
21
+
22
+ if (provider) {
23
+ return generateText({
24
+ model: provider(model),
25
+ system,
26
+ messages
27
+ });
28
+ }
29
+
30
+ const headers = { 'Content-Type': 'application/json' };
31
+ const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/getChatCompletion', {
32
+ method: 'POST',
33
+ headers,
34
+ body: JSON.stringify({
35
+ messages,
36
+ model: options?.model
37
+ })
38
+ }).then(response => {
39
+ if (!response.ok) {
40
+ return response.json().then(data => {
41
+ throw new Error(`Mongoose Studio chat completion error: ${data.message}`);
42
+ });
43
+ }
44
+ return response;
45
+ });
46
+
47
+ return await response.json().then(res => ({ text: res.response }));
48
+ };