@mongoosejs/studio 0.3.11 → 0.4.1
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.
- package/README.md +9 -0
- package/backend/actions/ChatThread/createChatMessage.js +15 -3
- package/backend/actions/ChatThread/createChatThread.js +8 -1
- package/backend/actions/ChatThread/index.js +1 -0
- package/backend/actions/ChatThread/streamChatMessage.js +49 -14
- package/backend/actions/ChatThread/toggleAgentMode.js +37 -0
- package/backend/actions/Model/getDocuments.js +7 -4
- package/backend/actions/Model/getDocumentsStream.js +7 -4
- package/backend/actions/Model/streamChatMessage.js +15 -4
- package/backend/authorize.js +1 -0
- package/backend/chatAgent/agentSystemPrompt.js +45 -0
- package/backend/chatAgent/agentToolMetadata.js +20 -0
- package/backend/chatAgent/getAgentTools.js +140 -0
- package/backend/db/chatMessageSchema.js +5 -0
- package/backend/db/chatThreadSchema.js +4 -0
- package/backend/helpers/isBindIPConnection.js +8 -0
- package/backend/helpers/isLocalhostConnection.js +8 -0
- package/backend/helpers/normalizeBindIPOption.js +16 -0
- package/backend/helpers/parseProjectionParam.js +180 -0
- package/backend/integrations/callLLM.js +4 -2
- package/backend/integrations/streamLLM.js +12 -4
- package/eslint.config.js +2 -1
- package/express.js +16 -0
- package/frontend/public/app.js +913 -110
- package/frontend/public/tw.css +34 -8
- package/frontend/src/api.js +6 -0
- package/frontend/src/chat/chat-message/chat-message.html +12 -1
- package/frontend/src/chat/chat-message/chat-message.js +8 -0
- package/frontend/src/chat/chat-message-script/chat-message-script.html +1 -1
- package/frontend/src/chat/chat.html +91 -44
- package/frontend/src/chat/chat.js +119 -19
- package/frontend/src/create-document/create-document.js +3 -0
- package/frontend/src/detail-objectid/detail-objectid.html +1 -0
- package/frontend/src/detail-objectid/detail-objectid.js +67 -0
- package/frontend/src/document-details/document-property/document-property.html +8 -3
- package/frontend/src/document-details/document-property/document-property.js +7 -0
- package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html +12 -0
- package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js +19 -0
- package/frontend/src/models/document-search/document-search.js +6 -0
- package/frontend/src/models/models.html +6 -8
- package/frontend/src/models/models.js +257 -85
- package/frontend/src/models/projection-search/projection-search.html +27 -0
- package/frontend/src/models/projection-search/projection-search.js +226 -0
- package/index.d.ts +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ For local dev, if your app runs on `localhost:3000`, Studio will be on `localhos
|
|
|
12
12
|
|
|
13
13
|
By default, Mongoose Studio does **not** provide any authentication or authorization.
|
|
14
14
|
You can use Mongoose Studio for free for local development, but we recommend [Mongoose Studio Pro](https://studio.mongoosejs.io/#pricing) for when you want to go into production.
|
|
15
|
+
When you omit an API key, Mongoose Studio only accepts localhost connections by default.
|
|
15
16
|
|
|
16
17
|
First, `npm install @mongoosejs/studio`.
|
|
17
18
|
|
|
@@ -43,6 +44,14 @@ opts.googleGeminiAPIKey = process.env.GOOGLE_GEMINI_API_KEY;
|
|
|
43
44
|
app.use('/studio', await studio('/studio/api', mongoose, opts));
|
|
44
45
|
```
|
|
45
46
|
|
|
47
|
+
Without an API key, you can allow access from additional IP addresses with `bindIp`, similar to MongoDB's `bindIp` option. `bindIp` may be a comma-separated string or an array of exact IP addresses. Set `bindIp` to `null` to allow unauthenticated connections from anywhere.
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
app.use('/studio', await studio('/studio/api', mongoose, {
|
|
51
|
+
bindIp: '127.0.0.1,192.168.0.10'
|
|
52
|
+
}));
|
|
53
|
+
```
|
|
54
|
+
|
|
46
55
|
### Next.js
|
|
47
56
|
|
|
48
57
|
First, add `withMongooseStudio` to your `next.config.js` file:
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const assert = require('assert');
|
|
5
5
|
const authorize = require('../../authorize');
|
|
6
|
+
const agentSystemPrompt = require('../../chatAgent/agentSystemPrompt');
|
|
6
7
|
const callLLM = require('../../integrations/callLLM');
|
|
8
|
+
const getAgentTools = require('../../chatAgent/getAgentTools');
|
|
7
9
|
const getModelDescriptions = require('../../helpers/getModelDescriptions');
|
|
8
10
|
const mongoose = require('mongoose');
|
|
9
11
|
|
|
@@ -81,12 +83,14 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
|
|
|
81
83
|
|
|
82
84
|
const modelDescriptions = getModelDescriptions(db);
|
|
83
85
|
const system = [
|
|
84
|
-
systemPrompt,
|
|
86
|
+
chatThread.agentMode ? agentSystemPrompt : systemPrompt,
|
|
85
87
|
currentDateTime ? `Current date: ${currentDateTime}` : null,
|
|
86
88
|
modelDescriptions,
|
|
87
89
|
options?.context
|
|
88
90
|
].filter(Boolean).join('\n\n');
|
|
89
91
|
|
|
92
|
+
const llmOptions = chatThread.agentMode ? { ...options, tools: getAgentTools(db) } : options;
|
|
93
|
+
|
|
90
94
|
// Create the chat message and get LLM response in parallel
|
|
91
95
|
const chatMessages = await Promise.all([
|
|
92
96
|
ChatMessage.create({
|
|
@@ -96,12 +100,20 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
|
|
|
96
100
|
script,
|
|
97
101
|
executionResult: null
|
|
98
102
|
}),
|
|
99
|
-
callLLM(llmMessages, system,
|
|
103
|
+
callLLM(llmMessages, system, llmOptions).then(res => {
|
|
100
104
|
const content = res.text;
|
|
105
|
+
const toolCalls = (res.steps || []).flatMap(step =>
|
|
106
|
+
(step.toolCalls || []).map(tc => ({
|
|
107
|
+
toolName: tc.toolName,
|
|
108
|
+
input: tc.toolName === 'typeCheck' ? {} : tc.args,
|
|
109
|
+
status: 'done'
|
|
110
|
+
}))
|
|
111
|
+
);
|
|
101
112
|
return ChatMessage.create({
|
|
102
113
|
chatThreadId,
|
|
103
114
|
role: 'assistant',
|
|
104
|
-
content
|
|
115
|
+
content,
|
|
116
|
+
toolCalls: toolCalls.length ? toolCalls : undefined
|
|
105
117
|
});
|
|
106
118
|
})
|
|
107
119
|
]);
|
|
@@ -22,11 +22,15 @@ const CreateChatThreadParams = new Archetype({
|
|
|
22
22
|
dashboardId: {
|
|
23
23
|
$type: mongoose.Types.ObjectId,
|
|
24
24
|
$required: false
|
|
25
|
+
},
|
|
26
|
+
agentMode: {
|
|
27
|
+
$type: 'boolean',
|
|
28
|
+
$required: false
|
|
25
29
|
}
|
|
26
30
|
}).compile('CreateChatThreadParams');
|
|
27
31
|
|
|
28
32
|
module.exports = ({ studioConnection }) => async function createChatThread(params) {
|
|
29
|
-
const { initiatedById, roles, $workspaceId, initialMessage, dashboardId } = new CreateChatThreadParams(params);
|
|
33
|
+
const { initiatedById, roles, $workspaceId, initialMessage, dashboardId, agentMode } = new CreateChatThreadParams(params);
|
|
30
34
|
const ChatThread = studioConnection.model('__Studio_ChatThread');
|
|
31
35
|
const ChatMessage = studioConnection.model('__Studio_ChatMessage');
|
|
32
36
|
|
|
@@ -42,6 +46,9 @@ module.exports = ({ studioConnection }) => async function createChatThread(param
|
|
|
42
46
|
if (dashboardId) {
|
|
43
47
|
doc.dashboardId = dashboardId;
|
|
44
48
|
}
|
|
49
|
+
if (agentMode != null) {
|
|
50
|
+
doc.agentMode = agentMode;
|
|
51
|
+
}
|
|
45
52
|
const chatThread = await ChatThread.create(doc);
|
|
46
53
|
|
|
47
54
|
if (initialMessage != null && initialMessage.trim() !== '') {
|
|
@@ -6,3 +6,4 @@ exports.getChatThread = require('./getChatThread');
|
|
|
6
6
|
exports.listChatThreads = require('./listChatThreads');
|
|
7
7
|
exports.shareChatThread = require('./shareChatThread');
|
|
8
8
|
exports.streamChatMessage = require('./streamChatMessage');
|
|
9
|
+
exports.toggleAgentMode = require('./toggleAgentMode');
|
|
@@ -4,6 +4,8 @@ const Archetype = require('archetype');
|
|
|
4
4
|
const assert = require('assert');
|
|
5
5
|
const authorize = require('../../authorize');
|
|
6
6
|
const callLLM = require('../../integrations/callLLM');
|
|
7
|
+
const agentSystemPrompt = require('../../chatAgent/agentSystemPrompt');
|
|
8
|
+
const getAgentTools = require('../../chatAgent/getAgentTools');
|
|
7
9
|
const streamLLM = require('../../integrations/streamLLM');
|
|
8
10
|
const getModelDescriptions = require('../../helpers/getModelDescriptions');
|
|
9
11
|
const mongoose = require('mongoose');
|
|
@@ -27,7 +29,7 @@ const CreateChatMessageParams = new Archetype({
|
|
|
27
29
|
}
|
|
28
30
|
}).compile('CreateChatMessageParams');
|
|
29
31
|
|
|
30
|
-
module.exports = ({ db, studioConnection, options }) => async function*
|
|
32
|
+
module.exports = ({ db, studioConnection, options }) => async function* streamChatMessage(params) {
|
|
31
33
|
const { chatThreadId, initiatedById, content, currentDateTime, script, roles } = new CreateChatMessageParams(params);
|
|
32
34
|
const ChatThread = studioConnection.model('__Studio_ChatThread');
|
|
33
35
|
const ChatMessage = studioConnection.model('__Studio_ChatMessage');
|
|
@@ -73,16 +75,20 @@ module.exports = ({ db, studioConnection, options }) => async function* createCh
|
|
|
73
75
|
}],
|
|
74
76
|
'You are a helpful assistant that summarizes chat threads into titles.',
|
|
75
77
|
options
|
|
76
|
-
).then(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
).then(
|
|
79
|
+
res => {
|
|
80
|
+
const title = res.text;
|
|
81
|
+
chatThread.title = title;
|
|
82
|
+
return chatThread.save();
|
|
83
|
+
},
|
|
84
|
+
// Avoid unhandled promise rejection
|
|
85
|
+
() => {}
|
|
86
|
+
);
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
const modelDescriptions = getModelDescriptions(db);
|
|
84
90
|
const system = [
|
|
85
|
-
systemPrompt,
|
|
91
|
+
chatThread.agentMode ? agentSystemPrompt : systemPrompt,
|
|
86
92
|
currentDateTime ? `Current date: ${currentDateTime}` : null,
|
|
87
93
|
modelDescriptions,
|
|
88
94
|
options?.context
|
|
@@ -105,16 +111,43 @@ module.exports = ({ db, studioConnection, options }) => async function* createCh
|
|
|
105
111
|
script: null,
|
|
106
112
|
executionResult: null
|
|
107
113
|
});
|
|
108
|
-
const
|
|
114
|
+
const llmOptions = chatThread.agentMode ? { ...options, tools: getAgentTools(db) } : options;
|
|
115
|
+
let textStream;
|
|
116
|
+
try {
|
|
117
|
+
textStream = streamLLM(llmMessages, system, llmOptions);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
yield { message: err?.message || 'Failed to stream response' };
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
const toolCalls = [];
|
|
109
123
|
let count = 0;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
try {
|
|
125
|
+
for await (const event of textStream) {
|
|
126
|
+
if (typeof event === 'string') {
|
|
127
|
+
assistantChatMessage.content += event;
|
|
128
|
+
// Only save every 10th chunk for performance
|
|
129
|
+
if (count++ % 10 === 0) {
|
|
130
|
+
await assistantChatMessage.save();
|
|
131
|
+
}
|
|
132
|
+
yield { textPart: event };
|
|
133
|
+
} else if (event.toolCall) {
|
|
134
|
+
toolCalls.push({ toolName: event.toolCall.toolName, input: event.toolCall.input, status: 'running' });
|
|
135
|
+
yield { toolCall: event.toolCall };
|
|
136
|
+
} else if (event.toolResult) {
|
|
137
|
+
const tc = toolCalls.find(t => t.toolName === event.toolResult.toolName && t.status === 'running');
|
|
138
|
+
if (tc) {
|
|
139
|
+
tc.status = 'done';
|
|
140
|
+
}
|
|
141
|
+
yield { toolResult: event.toolResult };
|
|
142
|
+
} else {
|
|
143
|
+
yield { textPart: event };
|
|
144
|
+
}
|
|
115
145
|
}
|
|
116
|
-
|
|
146
|
+
} catch (err) {
|
|
147
|
+
yield { message: err?.message || 'Failed to stream chat response' };
|
|
148
|
+
return {};
|
|
117
149
|
}
|
|
150
|
+
assistantChatMessage.toolCalls = toolCalls;
|
|
118
151
|
|
|
119
152
|
await assistantChatMessage.save();
|
|
120
153
|
yield { chatMessage: assistantChatMessage };
|
|
@@ -139,6 +172,8 @@ const systemPrompt = `
|
|
|
139
172
|
|
|
140
173
|
Assume the user has pre-defined schemas and models. Do not define any new schemas or models for the user.
|
|
141
174
|
|
|
175
|
+
Avoid using the aggregation framework unless explicitly asked by the user.
|
|
176
|
+
|
|
142
177
|
Use async/await where possible. Assume top-level await is allowed.
|
|
143
178
|
|
|
144
179
|
Write at most one script, unless the user explicitly asks for multiple scripts.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Archetype = require('archetype');
|
|
4
|
+
const authorize = require('../../authorize');
|
|
5
|
+
const mongoose = require('mongoose');
|
|
6
|
+
|
|
7
|
+
const ToggleAgentModeParams = new Archetype({
|
|
8
|
+
chatThreadId: {
|
|
9
|
+
$type: mongoose.Types.ObjectId
|
|
10
|
+
},
|
|
11
|
+
agentMode: {
|
|
12
|
+
$type: 'boolean'
|
|
13
|
+
},
|
|
14
|
+
initiatedById: {
|
|
15
|
+
$type: mongoose.Types.ObjectId
|
|
16
|
+
},
|
|
17
|
+
roles: {
|
|
18
|
+
$type: ['string']
|
|
19
|
+
}
|
|
20
|
+
}).compile('ToggleAgentModeParams');
|
|
21
|
+
|
|
22
|
+
module.exports = ({ studioConnection }) => async function toggleAgentMode(params) {
|
|
23
|
+
const { chatThreadId, agentMode, initiatedById, roles } = new ToggleAgentModeParams(params);
|
|
24
|
+
const ChatThread = studioConnection.model('__Studio_ChatThread');
|
|
25
|
+
|
|
26
|
+
await authorize('ChatThread.toggleAgentMode', roles);
|
|
27
|
+
|
|
28
|
+
const chatThread = await ChatThread.findById(chatThreadId).orFail();
|
|
29
|
+
if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
|
|
30
|
+
throw new Error('Not authorized');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
chatThread.agentMode = agentMode;
|
|
34
|
+
await chatThread.save();
|
|
35
|
+
|
|
36
|
+
return { chatThread };
|
|
37
|
+
};
|
|
@@ -5,7 +5,7 @@ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
|
|
|
5
5
|
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
6
6
|
const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
|
|
7
7
|
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
|
|
8
|
-
const
|
|
8
|
+
const parseProjectionParam = require('../../helpers/parseProjectionParam');
|
|
9
9
|
const authorize = require('../../authorize');
|
|
10
10
|
|
|
11
11
|
const GetDocumentsParams = new Archetype({
|
|
@@ -32,7 +32,7 @@ const GetDocumentsParams = new Archetype({
|
|
|
32
32
|
sortDirection: {
|
|
33
33
|
$type: 'number'
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
projectionInput: {
|
|
36
36
|
$type: 'string'
|
|
37
37
|
},
|
|
38
38
|
roles: {
|
|
@@ -45,7 +45,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
45
45
|
const { roles } = params;
|
|
46
46
|
await authorize('Model.getDocuments', roles);
|
|
47
47
|
|
|
48
|
-
const { model, limit, skip, sortKey, sortDirection, searchText,
|
|
48
|
+
const { model, limit, skip, sortKey, sortDirection, searchText, projectionInput } = params;
|
|
49
49
|
|
|
50
50
|
const Model = db.models[model];
|
|
51
51
|
if (Model == null) {
|
|
@@ -68,7 +68,10 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj);
|
|
71
|
-
const projection =
|
|
71
|
+
const projection = parseProjectionParam({
|
|
72
|
+
projectionInput,
|
|
73
|
+
schemaPaths: Object.keys(Model.schema.paths)
|
|
74
|
+
});
|
|
72
75
|
if (projection != null) {
|
|
73
76
|
query = query.select(projection);
|
|
74
77
|
}
|
|
@@ -5,7 +5,7 @@ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
|
|
|
5
5
|
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
6
6
|
const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
|
|
7
7
|
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
|
|
8
|
-
const
|
|
8
|
+
const parseProjectionParam = require('../../helpers/parseProjectionParam');
|
|
9
9
|
const authorize = require('../../authorize');
|
|
10
10
|
|
|
11
11
|
const GetDocumentsParams = new Archetype({
|
|
@@ -32,7 +32,7 @@ const GetDocumentsParams = new Archetype({
|
|
|
32
32
|
sortDirection: {
|
|
33
33
|
$type: 'number'
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
projectionInput: {
|
|
36
36
|
$type: 'string'
|
|
37
37
|
},
|
|
38
38
|
roles: {
|
|
@@ -45,7 +45,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
|
|
|
45
45
|
const { roles } = params;
|
|
46
46
|
await authorize('Model.getDocumentsStream', roles);
|
|
47
47
|
|
|
48
|
-
const { model, limit, skip, sortKey, sortDirection, searchText,
|
|
48
|
+
const { model, limit, skip, sortKey, sortDirection, searchText, projectionInput } = params;
|
|
49
49
|
|
|
50
50
|
const Model = db.models[model];
|
|
51
51
|
if (Model == null) {
|
|
@@ -68,7 +68,10 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj).batchSize(1);
|
|
71
|
-
const projection =
|
|
71
|
+
const projection = parseProjectionParam({
|
|
72
|
+
projectionInput,
|
|
73
|
+
schemaPaths: Object.keys(Model.schema.paths)
|
|
74
|
+
});
|
|
72
75
|
if (projection != null) {
|
|
73
76
|
query = query.select(projection);
|
|
74
77
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const assert = require('assert');
|
|
5
5
|
const authorize = require('../../authorize');
|
|
6
|
+
const getAgentTools = require('../../chatAgent/getAgentTools');
|
|
6
7
|
const streamLLM = require('../../integrations/streamLLM');
|
|
7
8
|
const getModelDescriptions = require('../../helpers/getModelDescriptions');
|
|
8
9
|
|
|
@@ -18,6 +19,10 @@ const StreamChatMessageParams = new Archetype({
|
|
|
18
19
|
documentData: {
|
|
19
20
|
$type: 'string'
|
|
20
21
|
},
|
|
22
|
+
agentMode: {
|
|
23
|
+
$type: 'boolean',
|
|
24
|
+
$default: true
|
|
25
|
+
},
|
|
21
26
|
currentDateTime: {
|
|
22
27
|
$type: 'string',
|
|
23
28
|
$transform: v => v == null ? null : decodeURIComponent(v),
|
|
@@ -29,7 +34,7 @@ const StreamChatMessageParams = new Archetype({
|
|
|
29
34
|
}).compile('StreamChatMessageParams');
|
|
30
35
|
|
|
31
36
|
module.exports = ({ db, options }) => async function* streamChatMessage(params) {
|
|
32
|
-
const { model, content, documentData, currentDateTime, roles } = new StreamChatMessageParams(params);
|
|
37
|
+
const { model, content, documentData, currentDateTime, agentMode, roles } = new StreamChatMessageParams(params);
|
|
33
38
|
|
|
34
39
|
await authorize('Model.streamChatMessage', roles);
|
|
35
40
|
|
|
@@ -51,10 +56,16 @@ module.exports = ({ db, options }) => async function* streamChatMessage(params)
|
|
|
51
56
|
].filter(Boolean).join('\n\n');
|
|
52
57
|
|
|
53
58
|
const llmMessages = [{ role: 'user', content: [{ type: 'text', text: content }] }];
|
|
54
|
-
const
|
|
59
|
+
const llmOptions = agentMode ? { ...options, tools: getAgentTools(db) } : options;
|
|
60
|
+
const textStream = streamLLM(llmMessages, system, llmOptions);
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
try {
|
|
63
|
+
for await (const textPart of textStream) {
|
|
64
|
+
yield { textPart };
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
yield { message: err?.message || 'Failed to stream chat response' };
|
|
68
|
+
return {};
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
return {};
|
package/backend/authorize.js
CHANGED
|
@@ -7,6 +7,7 @@ const actionsToRequiredRoles = {
|
|
|
7
7
|
'ChatThread.getChatThread': ['owner', 'admin', 'member'],
|
|
8
8
|
'ChatThread.listChatThreads': ['owner', 'admin', 'member'],
|
|
9
9
|
'ChatThread.shareChatThread': ['owner', 'admin', 'member'],
|
|
10
|
+
'ChatThread.toggleAgentMode': ['owner', 'admin', 'member'],
|
|
10
11
|
'Dashboard.createDashboard': ['owner', 'admin', 'member'],
|
|
11
12
|
'Dashboard.deleteDashboard': ['owner', 'admin', 'member'],
|
|
12
13
|
'Dashboard.getDashboard': ['owner', 'admin', 'member', 'readonly', 'dashboards'],
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = `
|
|
4
|
+
You are a data querying assistant with access to tools that can query MongoDB directly. Use these tools to investigate the user's data before writing a final script. The final script must be written in JavaScript.
|
|
5
|
+
|
|
6
|
+
Your tools are for EXPLORATION ONLY — use them to understand the data before writing a script. NEVER use tool results as the data source for the final script. The final script must be fully self-contained: it must query MongoDB itself and not depend on any data you retrieved via tools.
|
|
7
|
+
|
|
8
|
+
Follow this process for each query:
|
|
9
|
+
|
|
10
|
+
1. **Identify models**: Based on the user's question and the model descriptions below, identify which models are relevant.
|
|
11
|
+
2. **Check document counts**: Use estimatedDocumentCount on the relevant models to understand data volume. This helps you decide whether a query is safe to run or needs limits/filters.
|
|
12
|
+
3. **Test assumptions**: Use find or findOne to sample real documents and verify your assumptions about the shape of the data, field values, and relationships. Do not guess field values when you can look them up.
|
|
13
|
+
4. **Type-check**: Before presenting the script, run it through the typeCheck tool to catch errors. The sandbox globals are: db (a mongoose.Connection — use db.model('ModelName') to get models), mongoose, ObjectId, console, and MongooseStudioChartColors. Fix any errors and re-check until clean.
|
|
14
|
+
5. **Write the final script**: Once you understand the data, produce a self-contained script that queries MongoDB directly. The script must NOT use hard-coded data from your tool calls — it must run its own queries.
|
|
15
|
+
|
|
16
|
+
Keep scripts concise. Avoid unnecessary comments, error handling, and temporary variables.
|
|
17
|
+
|
|
18
|
+
Do not write any imports or require() statements, that will cause the script to break.
|
|
19
|
+
|
|
20
|
+
If the user approves the script, the script will run in the Node.js server in a sandboxed vm.createContext() call with the following globals: db (the Mongoose connection), mongoose, ObjectId (mongoose.Types.ObjectId), console, and MongooseStudioChartColors (an array of 8 hex color strings for chart dataset colors). 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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
Avoid using the aggregation framework unless explicitly asked by the user.
|
|
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
|
+
Write at most one script, unless the user explicitly asks for multiple scripts.
|
|
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. Set responsive: true, maintainAspectRatio: false options unless the user explicitly asks. Use MongooseStudioChartColors for dataset backgroundColor and borderColor by default. For line/bar charts, use MongooseStudioChartColors[i] as borderColor and MongooseStudioChartColors[i] + '33' as backgroundColor for each dataset. For pie/doughnut charts, use MongooseStudioChartColors.slice(0, data.length) as backgroundColor. Only use custom colors if the user explicitly requests specific colors.
|
|
37
|
+
|
|
38
|
+
If the user's query is best answered by a map, return an object { $featureCollection } which contains a GeoJSON FeatureCollection
|
|
39
|
+
|
|
40
|
+
If the user's query is best answered by a table, return an object { $table: { columns: string[], rows: any[][] } }
|
|
41
|
+
|
|
42
|
+
-----------
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
`.trim();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = [
|
|
4
|
+
{
|
|
5
|
+
name: 'estimatedDocumentCount',
|
|
6
|
+
description: 'Count the estimated number of documents in a collection. Times out after 5 seconds.'
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'find',
|
|
10
|
+
description: 'Run a find() query on a Mongoose model and return an array of documents. Times out after 5 seconds.'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'findOne',
|
|
14
|
+
description: 'Run a findOne() query on a Mongoose model and return a single document or null. Times out after 5 seconds.'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'typeCheck',
|
|
18
|
+
description: 'Type-checks the script and validates syntax using TypeScript and node --check'
|
|
19
|
+
}
|
|
20
|
+
];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const vm = require('vm');
|
|
4
|
+
const ts = require('typescript');
|
|
5
|
+
const { tool } = require('ai');
|
|
6
|
+
const { jsonSchema } = require('@ai-sdk/provider-utils');
|
|
7
|
+
const agentToolMetadata = require('./agentToolMetadata');
|
|
8
|
+
|
|
9
|
+
module.exports = function getAgentTools(db) {
|
|
10
|
+
const modelNames = Object.keys(db.models).filter(name => !name.startsWith('__Studio'));
|
|
11
|
+
const toolDescriptions = Object.fromEntries(agentToolMetadata.map(tool => [tool.name, tool.description]));
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
estimatedDocumentCount: tool({
|
|
15
|
+
description: toolDescriptions.estimatedDocumentCount + ' Takes a model name.',
|
|
16
|
+
inputSchema: jsonSchema({
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
modelName: { type: 'string', description: 'The Mongoose model name' }
|
|
20
|
+
},
|
|
21
|
+
required: ['modelName']
|
|
22
|
+
}),
|
|
23
|
+
execute: async({ modelName }) => {
|
|
24
|
+
console.log(`estimatedDocumentCount: modelName=${modelName}`);
|
|
25
|
+
const Model = db.models[modelName];
|
|
26
|
+
if (Model == null) {
|
|
27
|
+
return { error: `Model ${modelName} not found. Available models: ${modelNames.join(', ')}` };
|
|
28
|
+
}
|
|
29
|
+
const count = await Model.estimatedDocumentCount().maxTimeMS(5000);
|
|
30
|
+
return { modelName, count };
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
find: tool({
|
|
34
|
+
description: toolDescriptions.find,
|
|
35
|
+
inputSchema: jsonSchema({
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
modelName: { type: 'string', description: 'The Mongoose model name' },
|
|
39
|
+
filter: { type: 'object', description: 'MongoDB query filter', default: {} },
|
|
40
|
+
limit: { type: 'number', description: 'Maximum number of documents to return', default: 10 }
|
|
41
|
+
},
|
|
42
|
+
required: ['modelName']
|
|
43
|
+
}),
|
|
44
|
+
execute: async({ modelName, filter = {}, limit = 10 }) => {
|
|
45
|
+
console.log(`find: modelName=${modelName}, filter=${JSON.stringify(filter)}, limit=${limit}`);
|
|
46
|
+
const Model = db.models[modelName];
|
|
47
|
+
if (Model == null) {
|
|
48
|
+
return { error: `Model ${modelName} not found. Available models: ${modelNames.join(', ')}` };
|
|
49
|
+
}
|
|
50
|
+
const docs = await Model.find(filter).limit(limit).maxTimeMS(5000);
|
|
51
|
+
return { modelName, count: docs.length, docs };
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
findOne: tool({
|
|
55
|
+
description: toolDescriptions.findOne,
|
|
56
|
+
inputSchema: jsonSchema({
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
modelName: { type: 'string', description: 'The Mongoose model name' },
|
|
60
|
+
filter: { type: 'object', description: 'MongoDB query filter', default: {} }
|
|
61
|
+
},
|
|
62
|
+
required: ['modelName']
|
|
63
|
+
}),
|
|
64
|
+
execute: async({ modelName, filter = {} }) => {
|
|
65
|
+
console.log(`findOne: modelName=${modelName}, filter=${JSON.stringify(filter)}`);
|
|
66
|
+
const Model = db.models[modelName];
|
|
67
|
+
if (Model == null) {
|
|
68
|
+
return { error: `Model ${modelName} not found. Available models: ${modelNames.join(', ')}` };
|
|
69
|
+
}
|
|
70
|
+
const doc = await Model.findOne(filter).maxTimeMS(5000);
|
|
71
|
+
return { modelName, doc };
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
typeCheck: tool({
|
|
75
|
+
description: toolDescriptions.typeCheck + ' The script will run in a sandbox with globals: db (mongoose.Connection), mongoose, ObjectId (mongoose.Types.ObjectId), console, and MongooseStudioChartColors (string[]). Pass the raw script body (no imports, no wrapping function). Returns any TypeScript errors or JavaScript syntax errors found. Remember that you should write JavaScript, NOT TypeScript. This tool is just to check for obvious errors.',
|
|
76
|
+
inputSchema: jsonSchema({
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
script: { type: 'string', description: 'The script to type-check' }
|
|
80
|
+
},
|
|
81
|
+
required: ['script']
|
|
82
|
+
}),
|
|
83
|
+
execute: async({ script }) => {
|
|
84
|
+
const wrapped = wrapScriptForTypeCheck(script);
|
|
85
|
+
const fileName = '__check.ts';
|
|
86
|
+
const compilerOptions = {
|
|
87
|
+
noEmit: true,
|
|
88
|
+
strict: false,
|
|
89
|
+
target: ts.ScriptTarget.ESNext,
|
|
90
|
+
module: ts.ModuleKind.ESNext,
|
|
91
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
92
|
+
esModuleInterop: true,
|
|
93
|
+
baseUrl: require('path').resolve(__dirname, '..', '..'),
|
|
94
|
+
paths: { mongoose: ['./node_modules/mongoose'] }
|
|
95
|
+
};
|
|
96
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
97
|
+
const originalGetSourceFile = host.getSourceFile;
|
|
98
|
+
host.getSourceFile = (name, languageVersion) => {
|
|
99
|
+
if (name === fileName) {
|
|
100
|
+
return ts.createSourceFile(name, wrapped, languageVersion);
|
|
101
|
+
}
|
|
102
|
+
return originalGetSourceFile.call(host, name, languageVersion);
|
|
103
|
+
};
|
|
104
|
+
const program = ts.createProgram([fileName], compilerOptions, host);
|
|
105
|
+
const diagnostics = ts.getPreEmitDiagnostics(program)
|
|
106
|
+
.filter(d => d.file?.fileName === fileName);
|
|
107
|
+
|
|
108
|
+
const errors = diagnostics.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'));
|
|
109
|
+
|
|
110
|
+
// Also check that the script is valid JavaScript (not TypeScript) via vm.Script
|
|
111
|
+
try {
|
|
112
|
+
new vm.Script('(async () => {\n' + script + '\n})()');
|
|
113
|
+
} catch (e) {
|
|
114
|
+
errors.push('JavaScript syntax error: ' + e.message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (errors.length === 0) {
|
|
118
|
+
return { ok: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('Errors', errors);
|
|
122
|
+
|
|
123
|
+
return { ok: false, errors };
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const wrapScriptForTypeCheck = (script) => `
|
|
130
|
+
import mongoose from 'mongoose';
|
|
131
|
+
declare const db: Omit<mongoose.Connection, 'model'> & {
|
|
132
|
+
model(name: string): mongoose.Model<any>;
|
|
133
|
+
};
|
|
134
|
+
declare const ObjectId: typeof mongoose.Types.ObjectId;
|
|
135
|
+
declare const console: Console;
|
|
136
|
+
declare const MongooseStudioChartColors: string[];
|
|
137
|
+
async function __script() {
|
|
138
|
+
${script}
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
@@ -20,6 +20,11 @@ const chatMessageSchema = new mongoose.Schema({
|
|
|
20
20
|
type: String,
|
|
21
21
|
required: false
|
|
22
22
|
},
|
|
23
|
+
toolCalls: [{
|
|
24
|
+
toolName: String,
|
|
25
|
+
input: mongoose.Schema.Types.Mixed,
|
|
26
|
+
status: String
|
|
27
|
+
}],
|
|
23
28
|
executionResult: {
|
|
24
29
|
output: mongoose.Schema.Types.Mixed,
|
|
25
30
|
logs: String,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function normalizeBindIPOption(bindIp) {
|
|
4
|
+
if (bindIp === null) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (bindIp === undefined) {
|
|
8
|
+
return ['localhost'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bindIps = Array.isArray(bindIp) ? bindIp : `${bindIp}`.split(',');
|
|
12
|
+
return bindIps.
|
|
13
|
+
flatMap(bindIp => `${bindIp}`.split(',')).
|
|
14
|
+
map(bindIp => bindIp.trim()).
|
|
15
|
+
filter(bindIp => bindIp.length > 0);
|
|
16
|
+
};
|