@mtharrison/loupe 1.3.0 → 1.5.0
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 +29 -3
- package/dist/client/app.css +237 -137
- package/dist/client/app.js +428 -361
- package/dist/index.js +75 -1
- package/dist/session-nav.d.ts +1 -1
- package/dist/session-nav.js +8 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +84 -39
- package/dist/utils.js +3 -1
- package/examples/fully-featured.js +533 -0
- package/package.json +3 -2
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
const { randomUUID } = require('node:crypto');
|
|
5
|
+
const readline = require('node:readline/promises');
|
|
6
|
+
const process = require('node:process');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
addSpanEvent,
|
|
10
|
+
endSpan,
|
|
11
|
+
getLocalLLMTracer,
|
|
12
|
+
recordException,
|
|
13
|
+
startSpan,
|
|
14
|
+
wrapChatModel,
|
|
15
|
+
} = require('../dist/index.js');
|
|
16
|
+
|
|
17
|
+
const DEMO_TIMEOUT_MS = 15000;
|
|
18
|
+
|
|
19
|
+
const GUIDE_DATA = {
|
|
20
|
+
kyoto: {
|
|
21
|
+
coffee: ['Weekenders Coffee Tominokoji', 'Kurasu Kyoto Stand'],
|
|
22
|
+
museum: 'Kyoto National Museum',
|
|
23
|
+
neighborhood: 'Gion',
|
|
24
|
+
pack: ['layers', 'comfortable walking shoes', 'a compact umbrella'],
|
|
25
|
+
walk: 'Philosopher\'s Path and Higashiyama backstreets',
|
|
26
|
+
},
|
|
27
|
+
lisbon: {
|
|
28
|
+
coffee: ['Hello, Kristof', 'The Folks'],
|
|
29
|
+
museum: 'MAAT',
|
|
30
|
+
neighborhood: 'Baixa and Chiado',
|
|
31
|
+
pack: ['layers', 'comfortable walking shoes', 'a light rain shell'],
|
|
32
|
+
walk: 'Alfama viewpoints loop',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
async function runFullyFeaturedExample(options = {}) {
|
|
37
|
+
process.env.LLM_TRACE_ENABLED ??= '1';
|
|
38
|
+
|
|
39
|
+
const destinationKey = normalizeDestinationKey(options.destination || 'Lisbon');
|
|
40
|
+
const guide = GUIDE_DATA[destinationKey];
|
|
41
|
+
if (!guide) {
|
|
42
|
+
throw new Error(`Unsupported destination "${String(options.destination)}". Try Lisbon or Kyoto.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const port = getPort(options.port);
|
|
46
|
+
const destination = formatDestination(destinationKey);
|
|
47
|
+
const sessionId = options.sessionId || `fully-featured-demo-${randomUUID().slice(0, 8)}`;
|
|
48
|
+
const keepAlive = options.keepAlive !== false;
|
|
49
|
+
const openBrowserEnabled = options.openBrowser !== false;
|
|
50
|
+
const tracer = getLocalLLMTracer({ port });
|
|
51
|
+
const serverInfo = await tracer.startServer();
|
|
52
|
+
|
|
53
|
+
if (!serverInfo?.url) {
|
|
54
|
+
throw new Error('Failed to start the Loupe dashboard.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log(`[demo] Loupe dashboard: ${serverInfo.url}`);
|
|
58
|
+
if (openBrowserEnabled) {
|
|
59
|
+
openBrowser(serverInfo.url);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log(`[demo] Session: ${sessionId}`);
|
|
63
|
+
log(`[demo] Destination: ${destination}`);
|
|
64
|
+
|
|
65
|
+
const baseTags = {
|
|
66
|
+
destination,
|
|
67
|
+
example: 'fully-featured',
|
|
68
|
+
workflow: 'weekend-planner',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const buildContext = (overrides = {}) => {
|
|
72
|
+
const { tags = {}, ...rest } = overrides;
|
|
73
|
+
return {
|
|
74
|
+
sessionId,
|
|
75
|
+
rootSessionId: sessionId,
|
|
76
|
+
rootActorId: 'travel-assistant',
|
|
77
|
+
actorId: 'travel-assistant',
|
|
78
|
+
actorType: 'assistant',
|
|
79
|
+
model: 'trip-planner-v2',
|
|
80
|
+
provider: 'mock-llm',
|
|
81
|
+
tags: {
|
|
82
|
+
...baseTags,
|
|
83
|
+
...tags,
|
|
84
|
+
},
|
|
85
|
+
tenantId: 'demo-travel',
|
|
86
|
+
userId: 'traveler-01',
|
|
87
|
+
...rest,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const researchModel = wrapChatModel(
|
|
92
|
+
{
|
|
93
|
+
async invoke(input) {
|
|
94
|
+
const requestedDestination = formatDestination(normalizeDestinationKey(input?.destination || destination));
|
|
95
|
+
const selectedGuide = GUIDE_DATA[normalizeDestinationKey(requestedDestination)] || guide;
|
|
96
|
+
return {
|
|
97
|
+
message: {
|
|
98
|
+
role: 'assistant',
|
|
99
|
+
content: [
|
|
100
|
+
`Base the trip around ${selectedGuide.neighborhood}.`,
|
|
101
|
+
`Coffee: ${selectedGuide.coffee.join(' and ')}.`,
|
|
102
|
+
`Walk: ${selectedGuide.walk}.`,
|
|
103
|
+
`Museum: ${selectedGuide.museum}.`,
|
|
104
|
+
].join(' '),
|
|
105
|
+
},
|
|
106
|
+
tool_calls: [],
|
|
107
|
+
usage: createUsage(18, 27),
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
async *stream(input) {
|
|
111
|
+
const requestedDestination = formatDestination(normalizeDestinationKey(input?.destination || destination));
|
|
112
|
+
const selectedGuide = GUIDE_DATA[normalizeDestinationKey(requestedDestination)] || guide;
|
|
113
|
+
const content = `Research highlights for ${requestedDestination}: ${selectedGuide.walk}, ${selectedGuide.museum}.`;
|
|
114
|
+
yield { type: 'begin', role: 'assistant' };
|
|
115
|
+
yield { type: 'chunk', content };
|
|
116
|
+
yield {
|
|
117
|
+
type: 'finish',
|
|
118
|
+
message: { role: 'assistant', content },
|
|
119
|
+
tool_calls: [],
|
|
120
|
+
usage: createUsage(10, 12),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
() =>
|
|
125
|
+
buildContext({
|
|
126
|
+
actorId: 'city-researcher',
|
|
127
|
+
actorType: 'tool',
|
|
128
|
+
model: 'city-researcher-v1',
|
|
129
|
+
tags: {
|
|
130
|
+
surface: 'research',
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
{ port },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const plannerModel = wrapChatModel(
|
|
137
|
+
{
|
|
138
|
+
async invoke(input) {
|
|
139
|
+
const researchStageId = startSpan(
|
|
140
|
+
buildContext({
|
|
141
|
+
stage: 'research',
|
|
142
|
+
tags: {
|
|
143
|
+
surface: 'research-stage',
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
{
|
|
147
|
+
mode: 'invoke',
|
|
148
|
+
name: 'workflow.research',
|
|
149
|
+
request: {
|
|
150
|
+
input: {
|
|
151
|
+
destination: input.destination,
|
|
152
|
+
interests: input.interests,
|
|
153
|
+
},
|
|
154
|
+
options: {
|
|
155
|
+
sources: ['city-guide', 'weather-notes'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{ port },
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
addSpanEvent(
|
|
163
|
+
researchStageId,
|
|
164
|
+
{
|
|
165
|
+
name: 'retrieval.hit',
|
|
166
|
+
attributes: {
|
|
167
|
+
guide: destination,
|
|
168
|
+
sources: 2,
|
|
169
|
+
},
|
|
170
|
+
payload: {
|
|
171
|
+
guide: destination,
|
|
172
|
+
sources: ['city-guide', 'weather-notes'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{ port },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
endSpan(
|
|
179
|
+
researchStageId,
|
|
180
|
+
{
|
|
181
|
+
summary: `Loaded local guide data for ${destination}.`,
|
|
182
|
+
usage: createUsage(6, 4, 0.0000005, 0.0000008),
|
|
183
|
+
},
|
|
184
|
+
{ port },
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const research = await researchModel.invoke(
|
|
188
|
+
{
|
|
189
|
+
destination: input.destination,
|
|
190
|
+
interests: input.interests,
|
|
191
|
+
messages: [
|
|
192
|
+
{
|
|
193
|
+
role: 'user',
|
|
194
|
+
content: `Research a two-day ${destination} itinerary for ${input.interests.join(', ')}.`,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
channel: 'research',
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const availabilitySpanId = startSpan(
|
|
204
|
+
buildContext({
|
|
205
|
+
actorId: 'availability-service',
|
|
206
|
+
actorType: 'service',
|
|
207
|
+
tags: {
|
|
208
|
+
surface: 'availability',
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
{
|
|
212
|
+
mode: 'invoke',
|
|
213
|
+
name: 'tool.availability-check',
|
|
214
|
+
request: {
|
|
215
|
+
input: {
|
|
216
|
+
destination: input.destination,
|
|
217
|
+
hotel: `${destination} Riverside House`,
|
|
218
|
+
},
|
|
219
|
+
options: {
|
|
220
|
+
timeoutMs: 250,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{ port },
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
throw Object.assign(new Error('Supplier timed out during live availability lookup.'), {
|
|
229
|
+
code: 'SUPPLIER_TIMEOUT',
|
|
230
|
+
status: 504,
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// Keep the demo moving while leaving one child span in the error state.
|
|
234
|
+
recordException(availabilitySpanId, error, { port });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const outputGuardrailId = startSpan(
|
|
238
|
+
buildContext({
|
|
239
|
+
guardrailType: 'output-policy',
|
|
240
|
+
tags: {
|
|
241
|
+
surface: 'output-guardrail',
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
{
|
|
245
|
+
mode: 'invoke',
|
|
246
|
+
name: 'guardrail.output',
|
|
247
|
+
request: {
|
|
248
|
+
input: {
|
|
249
|
+
draft: research.message.content,
|
|
250
|
+
},
|
|
251
|
+
options: {
|
|
252
|
+
policy: 'travel-safe',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{ port },
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
addSpanEvent(
|
|
260
|
+
outputGuardrailId,
|
|
261
|
+
{
|
|
262
|
+
name: 'guardrail.review',
|
|
263
|
+
attributes: {
|
|
264
|
+
outcome: 'pass',
|
|
265
|
+
policy: 'travel-safe',
|
|
266
|
+
},
|
|
267
|
+
payload: {
|
|
268
|
+
removedClaims: 0,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{ port },
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
endSpan(
|
|
275
|
+
outputGuardrailId,
|
|
276
|
+
{
|
|
277
|
+
reason: 'Advice stays within local-demo safety rules.',
|
|
278
|
+
result: 'pass',
|
|
279
|
+
},
|
|
280
|
+
{ port },
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
message: {
|
|
285
|
+
role: 'assistant',
|
|
286
|
+
content: [
|
|
287
|
+
`Day 1: start in ${guide.neighborhood} with ${guide.coffee[0]}, then walk ${guide.walk}.`,
|
|
288
|
+
`Day 2: visit ${guide.museum}, then use ${guide.coffee[1]} as a reset stop.`,
|
|
289
|
+
research.message.content,
|
|
290
|
+
'One live availability lookup failed, so confirm bookings directly before you go.',
|
|
291
|
+
`Pack ${guide.pack.join(', ')}.`,
|
|
292
|
+
].join('\n'),
|
|
293
|
+
},
|
|
294
|
+
tool_calls: [],
|
|
295
|
+
usage: createUsage(42, 68),
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
async *stream(input) {
|
|
299
|
+
const parts = [
|
|
300
|
+
`Day 1 in ${destination}: ${guide.coffee[0]}, then ${guide.walk}. `,
|
|
301
|
+
`Day 2: ${guide.museum}, followed by time around ${guide.neighborhood}. `,
|
|
302
|
+
`Pack ${guide.pack.join(', ')}.`,
|
|
303
|
+
];
|
|
304
|
+
const finalContent = parts.join('');
|
|
305
|
+
|
|
306
|
+
yield { type: 'begin', role: 'assistant' };
|
|
307
|
+
for (const content of parts) {
|
|
308
|
+
yield { type: 'chunk', content };
|
|
309
|
+
}
|
|
310
|
+
yield {
|
|
311
|
+
type: 'finish',
|
|
312
|
+
message: { role: 'assistant', content: finalContent },
|
|
313
|
+
tool_calls: [],
|
|
314
|
+
usage: createUsage(24, 39),
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
() =>
|
|
319
|
+
buildContext({
|
|
320
|
+
tags: {
|
|
321
|
+
surface: 'planner',
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
{ port },
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const invokeInput = {
|
|
328
|
+
days: 2,
|
|
329
|
+
destination,
|
|
330
|
+
interests: ['coffee', 'walking', 'museum'],
|
|
331
|
+
messages: [
|
|
332
|
+
{
|
|
333
|
+
role: 'user',
|
|
334
|
+
content: `Plan a safe two-day ${destination} trip with coffee, walking, and one museum stop.`,
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const inputGuardrailId = startSpan(
|
|
340
|
+
buildContext({
|
|
341
|
+
guardrailType: 'input-policy',
|
|
342
|
+
tags: {
|
|
343
|
+
surface: 'input-guardrail',
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
{
|
|
347
|
+
mode: 'invoke',
|
|
348
|
+
name: 'guardrail.input',
|
|
349
|
+
request: {
|
|
350
|
+
input: invokeInput,
|
|
351
|
+
options: {
|
|
352
|
+
policy: 'travel-safe',
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{ port },
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
addSpanEvent(
|
|
360
|
+
inputGuardrailId,
|
|
361
|
+
{
|
|
362
|
+
name: 'guardrail.review',
|
|
363
|
+
attributes: {
|
|
364
|
+
outcome: 'pass',
|
|
365
|
+
policy: 'travel-safe',
|
|
366
|
+
},
|
|
367
|
+
payload: {
|
|
368
|
+
matchedRules: ['safe-travel'],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{ port },
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
endSpan(
|
|
375
|
+
inputGuardrailId,
|
|
376
|
+
{
|
|
377
|
+
reason: 'The prompt is safe to answer.',
|
|
378
|
+
result: 'pass',
|
|
379
|
+
},
|
|
380
|
+
{ port },
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const invokeResponse = await plannerModel.invoke(invokeInput, {
|
|
384
|
+
channel: 'planning',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
log('');
|
|
388
|
+
log('[demo] Invoke answer:');
|
|
389
|
+
log(invokeResponse.message.content);
|
|
390
|
+
|
|
391
|
+
const streamInput = {
|
|
392
|
+
...invokeInput,
|
|
393
|
+
messages: [
|
|
394
|
+
{
|
|
395
|
+
role: 'user',
|
|
396
|
+
content: `Now stream the concise version of the ${destination} plan.`,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const streamChunks = [];
|
|
402
|
+
for await (const chunk of plannerModel.stream(streamInput, { channel: 'handoff' })) {
|
|
403
|
+
streamChunks.push(chunk);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const streamReply = extractStreamText(streamChunks);
|
|
407
|
+
log('');
|
|
408
|
+
log('[demo] Stream answer:');
|
|
409
|
+
log(streamReply);
|
|
410
|
+
|
|
411
|
+
const traceCount = tracer.store
|
|
412
|
+
.list()
|
|
413
|
+
.items.filter((item) => item.hierarchy.sessionId === sessionId && item.tags.example === 'fully-featured').length;
|
|
414
|
+
|
|
415
|
+
log('');
|
|
416
|
+
log(`[demo] Recorded ${traceCount} traces for one session across guardrails, nested calls, errors, and streaming.`);
|
|
417
|
+
|
|
418
|
+
if (keepAlive) {
|
|
419
|
+
log(`[demo] Keep this process alive while you inspect ${serverInfo.url}`);
|
|
420
|
+
await waitForDashboardExit(serverInfo.url);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
invokeReply: invokeResponse.message.content,
|
|
425
|
+
sessionId,
|
|
426
|
+
streamReply,
|
|
427
|
+
traceCount,
|
|
428
|
+
url: serverInfo.url,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function createUsage(promptTokens, completionTokens, promptPrice = 0.000001, completionPrice = 0.000002) {
|
|
433
|
+
return {
|
|
434
|
+
pricing: {
|
|
435
|
+
completion: completionPrice,
|
|
436
|
+
prompt: promptPrice,
|
|
437
|
+
},
|
|
438
|
+
tokens: {
|
|
439
|
+
completion: completionTokens,
|
|
440
|
+
prompt: promptTokens,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function extractStreamText(chunks) {
|
|
446
|
+
return chunks
|
|
447
|
+
.filter((chunk) => chunk?.type === 'chunk' && typeof chunk.content === 'string')
|
|
448
|
+
.map((chunk) => chunk.content)
|
|
449
|
+
.join('');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function normalizeDestinationKey(value) {
|
|
453
|
+
return String(value || '')
|
|
454
|
+
.trim()
|
|
455
|
+
.toLowerCase();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function formatDestination(value) {
|
|
459
|
+
return String(value)
|
|
460
|
+
.split(' ')
|
|
461
|
+
.filter(Boolean)
|
|
462
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
463
|
+
.join(' ');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function getPort(port) {
|
|
467
|
+
const explicit = Number(port ?? process.env.LLM_TRACE_PORT);
|
|
468
|
+
return Number.isFinite(explicit) && explicit > 0 ? explicit : 4319;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function openBrowser(url) {
|
|
472
|
+
if (!process.stdout.isTTY || process.env.CI || process.env.LOUPE_OPEN_BROWSER === '0') {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const command =
|
|
477
|
+
process.platform === 'darwin'
|
|
478
|
+
? ['open', [url]]
|
|
479
|
+
: process.platform === 'win32'
|
|
480
|
+
? ['cmd', ['/c', 'start', '', url]]
|
|
481
|
+
: process.platform === 'linux'
|
|
482
|
+
? ['xdg-open', [url]]
|
|
483
|
+
: null;
|
|
484
|
+
|
|
485
|
+
if (!command) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const child = spawn(command[0], command[1], {
|
|
491
|
+
detached: true,
|
|
492
|
+
stdio: 'ignore',
|
|
493
|
+
});
|
|
494
|
+
child.on('error', () => {});
|
|
495
|
+
child.unref();
|
|
496
|
+
} catch (_error) {
|
|
497
|
+
// Ignore browser launch failures. The dashboard URL is already printed.
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function waitForDashboardExit(url) {
|
|
502
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
503
|
+
log(`[demo] Non-interactive terminal detected. Leaving the dashboard up for ${Math.round(DEMO_TIMEOUT_MS / 1000)} seconds: ${url}`);
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, DEMO_TIMEOUT_MS));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const rl = readline.createInterface({
|
|
509
|
+
input: process.stdin,
|
|
510
|
+
output: process.stdout,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await rl.question('[demo] Press Enter to stop the demo and close the dashboard.\n');
|
|
515
|
+
} finally {
|
|
516
|
+
rl.close();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function log(message) {
|
|
521
|
+
process.stdout.write(`${message}\n`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
module.exports = {
|
|
525
|
+
runFullyFeaturedExample,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
if (require.main === module) {
|
|
529
|
+
runFullyFeaturedExample().catch((error) => {
|
|
530
|
+
process.stderr.write(`[demo] ${error.message}\n`);
|
|
531
|
+
process.exitCode = 1;
|
|
532
|
+
});
|
|
533
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mtharrison/loupe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Lightweight local tracing dashboard for LLM calls",
|
|
5
5
|
"author": "Matt Harrison",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,13 +24,14 @@
|
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc -p tsconfig.json && node scripts/build-ui.mjs",
|
|
34
|
+
"lint": "tsc --noEmit -p tsconfig.json && node --check examples/fully-featured.js && node --check examples/nested-tool-call.js && node --check examples/openai-multiturn-tools.js && node --check test/index.test.js",
|
|
34
35
|
"release": "npx semantic-release",
|
|
35
36
|
"test": "npm run build && node --test"
|
|
36
37
|
},
|