@mongoosejs/studio 0.5.1 → 0.5.3

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.
Files changed (42) hide show
  1. package/backend/actions/ChatThread/streamChatMessage.js +12 -12
  2. package/backend/actions/Dashboard/getDashboard.js +29 -2
  3. package/backend/actions/Model/dropCollection.js +29 -0
  4. package/backend/actions/Model/index.js +1 -0
  5. package/backend/authorize.js +1 -0
  6. package/backend/chatAgent/agentSystemPrompt.js +22 -10
  7. package/backend/chatAgent/getAgentTools.js +19 -8
  8. package/backend/chatAgent/runChatAgent.js +17 -0
  9. package/backend/db/dashboardSchema.js +0 -32
  10. package/backend/index.js +4 -1
  11. package/backend/integrations/callLLM.js +5 -20
  12. package/backend/integrations/streamLLM.js +5 -24
  13. package/backend/sandbox/createSandbox.js +6 -0
  14. package/backend/vercel.js +3 -0
  15. package/debugging.js +11 -0
  16. package/eval/agent/cases.js +168 -0
  17. package/eval/agent/fixtures/carsharing.js +147 -0
  18. package/eval/agent/fixtures/ecommerce.js +152 -0
  19. package/eval/agent/run.js +323 -0
  20. package/frontend/index.js +2 -1
  21. package/frontend/public/app.js +581 -203
  22. package/frontend/public/tw.css +81 -0
  23. package/frontend/src/_util/recent-pages-history.js +116 -0
  24. package/frontend/src/api.js +96 -164
  25. package/frontend/src/chat/chat-message-script/chat-message-script.html +1 -6
  26. package/frontend/src/chat/chat.html +1 -1
  27. package/frontend/src/document/confirm-changes/confirm-changes.js +20 -0
  28. package/frontend/src/document/document.html +2 -2
  29. package/frontend/src/index.js +148 -1
  30. package/frontend/src/json-node/json-node.html +41 -39
  31. package/frontend/src/json-node/json-node.js +38 -0
  32. package/frontend/src/list-json/list-json.html +3 -2
  33. package/frontend/src/list-json/list-json.js +5 -0
  34. package/frontend/src/modal/modal.css +26 -1
  35. package/frontend/src/modal/modal.html +2 -2
  36. package/frontend/src/modal/modal.js +5 -1
  37. package/frontend/src/models/models.html +45 -5
  38. package/frontend/src/models/models.js +26 -0
  39. package/frontend/src/navbar/navbar.html +92 -5
  40. package/frontend/src/navbar/navbar.js +69 -1
  41. package/index.js +2 -1
  42. package/package.json +3 -2
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const mongoose = require('mongoose');
7
+ const { MongoMemoryReplSet } = require('mongodb-memory-server');
8
+
9
+ const studio = require('../..');
10
+ const runChatAgent = require('../../backend/chatAgent/runChatAgent');
11
+ const createSandbox = require('../../backend/sandbox/createSandbox');
12
+ const allCases = require('./cases');
13
+ const fixtures = {
14
+ ecommerce: require('./fixtures/ecommerce'),
15
+ carsharing: require('./fixtures/carsharing')
16
+ };
17
+
18
+ const args = parseArgs(process.argv.slice(2));
19
+
20
+ studio.enableDebugging();
21
+
22
+ main().catch(err => {
23
+ console.error(err);
24
+ process.exit(1);
25
+ });
26
+
27
+ async function main() {
28
+ if (args.help) {
29
+ printHelp();
30
+ process.exit(0);
31
+ }
32
+
33
+ const llmOptions = buildLlmOptionsFromEnv();
34
+ if (!llmOptions) {
35
+ console.error('No LLM API key found. Set one of:');
36
+ console.error(' ANTHROPIC_API_KEY');
37
+ console.error(' OPENAI_API_KEY');
38
+ console.error(' GOOGLE_GEMINI_API_KEY');
39
+ process.exit(1);
40
+ }
41
+ if (args.model) {
42
+ llmOptions.model = args.model;
43
+ }
44
+
45
+ const selected = filterCases(allCases, args);
46
+ if (selected.length === 0) {
47
+ console.error('No cases matched the filter.');
48
+ process.exit(1);
49
+ }
50
+ const runs = args.runs ?? 1;
51
+
52
+ let replSet = null;
53
+ let uri = process.env.MONGODB_URI;
54
+ if (!uri) {
55
+ console.log('Starting in-memory MongoDB replSet...');
56
+ replSet = await MongoMemoryReplSet.create({
57
+ replSet: { name: 'rs0', count: 1 },
58
+ binary: { version: process.env.MONGOMS_VERSION || '8.0.0' }
59
+ });
60
+ uri = replSet.getUri();
61
+ }
62
+
63
+ const runId = new Date().toISOString().replace(/[:.]/g, '-');
64
+ const traceDir = args.noTraces ? null : path.join(__dirname, 'traces', runId);
65
+ if (traceDir) {
66
+ fs.mkdirSync(traceDir, { recursive: true });
67
+ console.log(`Writing traces to ${traceDir}`);
68
+ }
69
+
70
+ console.log(`Running ${selected.length} case(s) × ${runs} run(s) using provider "${llmOptions.providerName}" (${llmOptions.model || 'default model'})\n`);
71
+
72
+ const results = [];
73
+ for (const caseDef of selected) {
74
+ for (let i = 0; i < runs; i++) {
75
+ const result = await runCase(uri, caseDef, llmOptions);
76
+ result.runIndex = i;
77
+ results.push(result);
78
+ if (traceDir) {
79
+ const filename = `${caseDef.id}${runs > 1 ? `__run${i}` : ''}.json`;
80
+ fs.writeFileSync(path.join(traceDir, filename), JSON.stringify(serializeResult(result), null, 2));
81
+ }
82
+ printCaseLine(result);
83
+ }
84
+ }
85
+
86
+ printSummary(results);
87
+
88
+ if (replSet) {
89
+ await replSet.stop();
90
+ }
91
+
92
+ const overallPass = results.filter(r => r.passed).length / results.length;
93
+ process.exit(overallPass >= (args.threshold ?? 0.8) ? 0 : 1);
94
+ }
95
+
96
+ async function runCase(uri, caseDef, llmOptions) {
97
+ const fixture = fixtures[caseDef.fixture];
98
+ if (!fixture) {
99
+ return {
100
+ caseDef,
101
+ trace: emptyTrace(caseDef),
102
+ assertionError: new Error(`unknown fixture "${caseDef.fixture}"`),
103
+ passed: false
104
+ };
105
+ }
106
+
107
+ const dbName = `eval_${caseDef.id.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}`;
108
+ const conn = mongoose.createConnection();
109
+ await conn.openUri(uri, { dbName });
110
+
111
+ let trace = emptyTrace(caseDef);
112
+ trace.model = llmOptions.model || null;
113
+ trace.provider = llmOptions.providerName;
114
+
115
+ const started = Date.now();
116
+ try {
117
+ const models = fixture.defineModels(conn);
118
+ await fixture.seed(models);
119
+
120
+ const stream = runChatAgent({
121
+ db: conn,
122
+ llmMessages: [{ role: 'user', content: [{ type: 'text', text: caseDef.prompt }] }],
123
+ currentDateTime: caseDef.currentDateTime ?? '2026-05-13T12:00:00',
124
+ options: llmOptions
125
+ });
126
+
127
+ for await (const event of stream) {
128
+ if (typeof event === 'string') {
129
+ trace.text += event;
130
+ } else if (event.toolCall) {
131
+ trace.toolCalls.push({ toolName: event.toolCall.toolName, input: event.toolCall.input });
132
+ } else if (event.toolResult) {
133
+ trace.toolResults.push({ toolName: event.toolResult.toolName, output: summarizeToolOutput(event.toolResult.output) });
134
+ }
135
+ }
136
+ } catch (err) {
137
+ trace.error = err?.message || String(err);
138
+ }
139
+ trace.durationMs = Date.now() - started;
140
+ trace.finalCode = extractFinalCode(trace.text);
141
+
142
+ await executeFinalCode(conn, trace);
143
+
144
+ try {
145
+ await conn.dropDatabase();
146
+ } catch (_) { /* ignore */ }
147
+ await conn.close();
148
+
149
+ let assertionError = null;
150
+ try {
151
+ caseDef.assert(trace);
152
+ } catch (err) {
153
+ assertionError = err;
154
+ }
155
+ const passed = !trace.error && !assertionError;
156
+
157
+ return { caseDef, trace, assertionError, passed };
158
+ }
159
+
160
+ async function executeFinalCode(conn, trace) {
161
+ if (!trace.finalCode) {
162
+ return;
163
+ }
164
+ const sandbox = createSandbox({ db: conn });
165
+ try {
166
+ trace.output = await sandbox.runScript({ script: trace.finalCode });
167
+ trace.logs = sandbox.getLogs();
168
+ } catch (err) {
169
+ trace.executionError = err?.message || String(err);
170
+ } finally {
171
+ try { await sandbox.close(); } catch (_) { /* ignore */ }
172
+ }
173
+ }
174
+
175
+ function emptyTrace(caseDef) {
176
+ return {
177
+ caseId: caseDef.id,
178
+ category: caseDef.category,
179
+ fixture: caseDef.fixture,
180
+ prompt: caseDef.prompt,
181
+ text: '',
182
+ toolCalls: [],
183
+ toolResults: [],
184
+ finalCode: null,
185
+ output: undefined,
186
+ logs: null,
187
+ executionError: null,
188
+ error: null,
189
+ durationMs: 0
190
+ };
191
+ }
192
+
193
+ function extractFinalCode(text) {
194
+ if (!text) return null;
195
+ const matches = [...text.matchAll(/```(?:javascript|js)?\s*\n([\s\S]*?)```/g)];
196
+ if (matches.length === 0) return null;
197
+ return matches[matches.length - 1][1].trim();
198
+ }
199
+
200
+ function summarizeToolOutput(output) {
201
+ if (output == null) return null;
202
+ if (typeof output !== 'object') return output;
203
+ const summary = {};
204
+ for (const [k, v] of Object.entries(output)) {
205
+ if (Array.isArray(v)) {
206
+ summary[k] = { __array: true, length: v.length, sample: v.slice(0, 2) };
207
+ } else if (v && typeof v === 'object') {
208
+ const str = safeStringify(v);
209
+ summary[k] = str.length > 500 ? str.slice(0, 500) + '...[truncated]' : v;
210
+ } else {
211
+ summary[k] = v;
212
+ }
213
+ }
214
+ return summary;
215
+ }
216
+
217
+ function safeStringify(v) {
218
+ try { return JSON.stringify(v); } catch (_) { return String(v); }
219
+ }
220
+
221
+ function serializeResult(result) {
222
+ return {
223
+ caseId: result.caseDef.id,
224
+ category: result.caseDef.category,
225
+ fixture: result.caseDef.fixture,
226
+ prompt: result.caseDef.prompt,
227
+ runIndex: result.runIndex,
228
+ passed: result.passed,
229
+ assertionError: result.assertionError
230
+ ? { message: result.assertionError.message, stack: result.assertionError.stack }
231
+ : null,
232
+ trace: result.trace
233
+ };
234
+ }
235
+
236
+ function buildLlmOptionsFromEnv() {
237
+ if (process.env.ANTHROPIC_API_KEY) {
238
+ return { anthropicAPIKey: process.env.ANTHROPIC_API_KEY, providerName: 'anthropic' };
239
+ }
240
+ if (process.env.OPENAI_API_KEY) {
241
+ return { openAIAPIKey: process.env.OPENAI_API_KEY, providerName: 'openai' };
242
+ }
243
+ if (process.env.GOOGLE_GEMINI_API_KEY) {
244
+ return { googleGeminiAPIKey: process.env.GOOGLE_GEMINI_API_KEY, providerName: 'google' };
245
+ }
246
+ return null;
247
+ }
248
+
249
+ function filterCases(cases, args) {
250
+ return cases.filter(c => {
251
+ if (args.id && c.id !== args.id) return false;
252
+ if (args.category && c.category !== args.category) return false;
253
+ return true;
254
+ });
255
+ }
256
+
257
+ function printCaseLine(result) {
258
+ const status = result.passed ? 'PASS' : 'FAIL';
259
+ const symbol = result.passed ? '✓' : '✗';
260
+ const dur = `${result.trace.durationMs}ms`;
261
+ const tools = `${result.trace.toolCalls.length} tool calls`;
262
+ console.log(` ${symbol} [${status}] ${result.caseDef.category}/${result.caseDef.id} — ${dur}, ${tools}`);
263
+ if (!result.passed) {
264
+ if (result.trace.error) {
265
+ console.log(` error: ${result.trace.error}`);
266
+ }
267
+ if (result.assertionError) {
268
+ console.log(` assertion: ${result.assertionError.message}`);
269
+ }
270
+ }
271
+ }
272
+
273
+ function printSummary(results) {
274
+ const byCategory = {};
275
+ for (const r of results) {
276
+ const cat = r.caseDef.category;
277
+ byCategory[cat] = byCategory[cat] || { passed: 0, total: 0 };
278
+ byCategory[cat].total += 1;
279
+ if (r.passed) byCategory[cat].passed += 1;
280
+ }
281
+ console.log('\n=== Summary ===');
282
+ for (const [cat, { passed, total }] of Object.entries(byCategory)) {
283
+ const pct = ((passed / total) * 100).toFixed(0);
284
+ console.log(` ${cat.padEnd(12)} ${passed}/${total} (${pct}%)`);
285
+ }
286
+ const totalPassed = results.filter(r => r.passed).length;
287
+ const pct = ((totalPassed / results.length) * 100).toFixed(0);
288
+ console.log(` ${'overall'.padEnd(12)} ${totalPassed}/${results.length} (${pct}%)`);
289
+ }
290
+
291
+ function parseArgs(argv) {
292
+ const out = {};
293
+ for (const arg of argv) {
294
+ if (arg === '--help' || arg === '-h') { out.help = true; continue; }
295
+ if (arg === '--no-traces') { out.noTraces = true; continue; }
296
+ const m = arg.match(/^--([a-zA-Z-]+)=(.*)$/);
297
+ if (m) {
298
+ const key = m[1].replace(/-([a-z])/g, (_, c) => c.toUpperCase());
299
+ const value = m[2];
300
+ out[key] = /^\d+$/.test(value) ? Number(value) : value;
301
+ }
302
+ }
303
+ return out;
304
+ }
305
+
306
+ function printHelp() {
307
+ console.log(`Usage: node eval/agent/run.js [options]
308
+
309
+ Options:
310
+ --id=<case-id> Run only the case with this id
311
+ --category=<name> Run only cases in this category (simple|nested|dashboard|ambiguous)
312
+ --runs=<n> Run each case N times (default 1)
313
+ --model=<id> Override LLM model (e.g. claude-haiku-4-5-20251001)
314
+ --threshold=<0..1> Exit non-zero if overall pass rate is below this (default 0.8)
315
+ --no-traces Don't write trace JSON files to disk
316
+ --help Show this help
317
+
318
+ Environment:
319
+ ANTHROPIC_API_KEY | OPENAI_API_KEY | GOOGLE_GEMINI_API_KEY (set exactly one)
320
+ MONGODB_URI Use an existing MongoDB instead of the in-memory replSet
321
+ MONGOMS_VERSION MongoDB version for the in-memory replSet
322
+ `);
323
+ }
package/frontend/index.js CHANGED
@@ -35,7 +35,8 @@ module.exports = async function frontend(apiUrl, isLambda, options, workspace) {
35
35
  baseURL: apiUrl,
36
36
  isLambda,
37
37
  mothershipUrl: apiKey ? mothershipUrl : null,
38
- workspace: workspaceData
38
+ workspace: workspaceData,
39
+ hasLLMAPIKey: [options?.anthropicAPIKey, options?.googleGeminiAPIKey, options?.openAIAPIKey].some(v => v != null)
39
40
  };
40
41
 
41
42
  if (isLambda) {