@mongoosejs/studio 0.5.2 → 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.
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const mongoose = require('mongoose');
4
+
5
+ const { Schema } = mongoose;
6
+
7
+ const userSchema = new Schema({
8
+ name: String,
9
+ email: String,
10
+ signupDate: Date,
11
+ isDeleted: { type: Boolean, default: false }
12
+ });
13
+
14
+ const productSchema = new Schema({
15
+ name: String,
16
+ category: { type: String, enum: ['electronics', 'apparel', 'home', 'books'] },
17
+ price: Number,
18
+ inventory: Number,
19
+ createdAt: Date
20
+ });
21
+
22
+ const orderSchema = new Schema({
23
+ userId: { type: Schema.Types.ObjectId, ref: 'User' },
24
+ items: [{
25
+ productId: { type: Schema.Types.ObjectId, ref: 'Product' },
26
+ quantity: Number,
27
+ price: Number
28
+ }],
29
+ total: Number,
30
+ status: { type: String, enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'] },
31
+ placedAt: Date,
32
+ paidAt: Date
33
+ });
34
+
35
+ module.exports = {
36
+ name: 'ecommerce',
37
+ defineModels(connection) {
38
+ return {
39
+ User: connection.model('User', userSchema),
40
+ Product: connection.model('Product', productSchema),
41
+ Order: connection.model('Order', orderSchema)
42
+ };
43
+ },
44
+ async seed({ User, Product, Order }) {
45
+ const users = await User.create([
46
+ { name: 'Alice Smith', email: 'alice@example.com', signupDate: new Date('2025-01-15'), isDeleted: false },
47
+ { name: 'Bob Jones', email: 'bob@example.com', signupDate: new Date('2025-04-22'), isDeleted: false },
48
+ { name: 'Carol Wu', email: 'carol@example.com', signupDate: new Date('2025-09-08'), isDeleted: true },
49
+ { name: 'David Park', email: 'david@example.com', signupDate: new Date('2025-12-03'), isDeleted: false }
50
+ ]);
51
+
52
+ const products = await Product.create([
53
+ { name: 'Laptop', category: 'electronics', price: 1200, inventory: 12, createdAt: new Date('2024-11-01') },
54
+ { name: 'Headphones', category: 'electronics', price: 150, inventory: 45, createdAt: new Date('2024-11-01') },
55
+ { name: 'T-Shirt', category: 'apparel', price: 25, inventory: 200, createdAt: new Date('2025-01-15') },
56
+ { name: 'Lamp', category: 'home', price: 60, inventory: 30, createdAt: new Date('2025-02-10') },
57
+ { name: 'Novel', category: 'books', price: 18, inventory: 100, createdAt: new Date('2025-03-04') }
58
+ ]);
59
+
60
+ const monthsAgo = (n) => {
61
+ const d = new Date('2026-05-13T12:00:00.000Z');
62
+ d.setUTCMonth(d.getUTCMonth() - n);
63
+ return d;
64
+ };
65
+
66
+ await Order.create([
67
+ {
68
+ userId: users[0]._id,
69
+ items: [{ productId: products[0]._id, quantity: 1, price: 1200 }],
70
+ total: 1200,
71
+ status: 'delivered',
72
+ placedAt: monthsAgo(5),
73
+ paidAt: monthsAgo(5)
74
+ },
75
+ {
76
+ userId: users[0]._id,
77
+ items: [{ productId: products[1]._id, quantity: 2, price: 150 }],
78
+ total: 300,
79
+ status: 'paid',
80
+ placedAt: monthsAgo(3),
81
+ paidAt: monthsAgo(3)
82
+ },
83
+ {
84
+ userId: users[1]._id,
85
+ items: [
86
+ { productId: products[2]._id, quantity: 3, price: 25 },
87
+ { productId: products[4]._id, quantity: 1, price: 18 }
88
+ ],
89
+ total: 93,
90
+ status: 'shipped',
91
+ placedAt: monthsAgo(4),
92
+ paidAt: monthsAgo(4)
93
+ },
94
+ {
95
+ userId: users[1]._id,
96
+ items: [{ productId: products[3]._id, quantity: 1, price: 60 }],
97
+ total: 60,
98
+ status: 'paid',
99
+ placedAt: monthsAgo(2),
100
+ paidAt: monthsAgo(2)
101
+ },
102
+ {
103
+ userId: users[3]._id,
104
+ items: [{ productId: products[0]._id, quantity: 1, price: 1200 }],
105
+ total: 1200,
106
+ status: 'cancelled',
107
+ placedAt: monthsAgo(2),
108
+ paidAt: null
109
+ },
110
+ {
111
+ userId: users[3]._id,
112
+ items: [{ productId: products[1]._id, quantity: 1, price: 150 }],
113
+ total: 150,
114
+ status: 'paid',
115
+ placedAt: monthsAgo(1),
116
+ paidAt: monthsAgo(1)
117
+ },
118
+ {
119
+ userId: users[1]._id,
120
+ items: [{ productId: products[4]._id, quantity: 4, price: 18 }],
121
+ total: 72,
122
+ status: 'paid',
123
+ placedAt: monthsAgo(1),
124
+ paidAt: monthsAgo(1)
125
+ },
126
+ {
127
+ userId: users[3]._id,
128
+ items: [{ productId: products[2]._id, quantity: 2, price: 25 }],
129
+ total: 50,
130
+ status: 'pending',
131
+ placedAt: monthsAgo(0),
132
+ paidAt: null
133
+ },
134
+ {
135
+ userId: users[0]._id,
136
+ items: [{ productId: products[3]._id, quantity: 2, price: 60 }],
137
+ total: 120,
138
+ status: 'cancelled',
139
+ placedAt: monthsAgo(6),
140
+ paidAt: null
141
+ },
142
+ {
143
+ userId: users[1]._id,
144
+ items: [{ productId: products[0]._id, quantity: 1, price: 1200 }],
145
+ total: 1200,
146
+ status: 'delivered',
147
+ placedAt: monthsAgo(4),
148
+ paidAt: monthsAgo(4)
149
+ }
150
+ ]);
151
+ }
152
+ };
@@ -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
+ }