@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
package/dist/index.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.isTraceEnabled = isTraceEnabled;
|
|
4
37
|
exports.getLocalLLMTracer = getLocalLLMTracer;
|
|
@@ -11,6 +44,7 @@ exports.__resetLocalLLMTracerForTests = __resetLocalLLMTracerForTests;
|
|
|
11
44
|
exports.wrapChatModel = wrapChatModel;
|
|
12
45
|
exports.wrapOpenAIClient = wrapOpenAIClient;
|
|
13
46
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
47
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
14
48
|
const server_1 = require("./server");
|
|
15
49
|
const store_1 = require("./store");
|
|
16
50
|
const ui_build_1 = require("./ui-build");
|
|
@@ -19,7 +53,10 @@ let singleton = null;
|
|
|
19
53
|
const DEFAULT_TRACE_PORT = 4319;
|
|
20
54
|
const activeSpanStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
21
55
|
function isTraceEnabled() {
|
|
22
|
-
|
|
56
|
+
if (process.env.LLM_TRACE_ENABLED !== undefined) {
|
|
57
|
+
return (0, utils_1.envFlag)('LLM_TRACE_ENABLED');
|
|
58
|
+
}
|
|
59
|
+
return process.env.NODE_ENV === 'development';
|
|
23
60
|
}
|
|
24
61
|
function getLocalLLMTracer(config = {}) {
|
|
25
62
|
if (!singleton) {
|
|
@@ -180,6 +217,7 @@ function wrapOpenAIClient(client, getContext, config) {
|
|
|
180
217
|
class LocalLLMTracerImpl {
|
|
181
218
|
config;
|
|
182
219
|
loggedUrl;
|
|
220
|
+
openedBrowser;
|
|
183
221
|
portWasExplicit;
|
|
184
222
|
server;
|
|
185
223
|
serverFailed;
|
|
@@ -202,6 +240,7 @@ class LocalLLMTracerImpl {
|
|
|
202
240
|
this.serverStartPromise = null;
|
|
203
241
|
this.serverFailed = false;
|
|
204
242
|
this.loggedUrl = false;
|
|
243
|
+
this.openedBrowser = false;
|
|
205
244
|
this.uiWatcher = null;
|
|
206
245
|
}
|
|
207
246
|
configure(config = {}) {
|
|
@@ -278,6 +317,10 @@ class LocalLLMTracerImpl {
|
|
|
278
317
|
this.loggedUrl = true;
|
|
279
318
|
process.stdout.write(`[llm-trace] dashboard: ${this.serverInfo.url}\n`);
|
|
280
319
|
}
|
|
320
|
+
if (!this.openedBrowser && this.serverInfo && shouldAutoOpenDashboard()) {
|
|
321
|
+
this.openedBrowser = true;
|
|
322
|
+
openBrowser(this.serverInfo.url);
|
|
323
|
+
}
|
|
281
324
|
return this.serverInfo;
|
|
282
325
|
}
|
|
283
326
|
catch (error) {
|
|
@@ -292,6 +335,37 @@ class LocalLLMTracerImpl {
|
|
|
292
335
|
return this.serverStartPromise;
|
|
293
336
|
}
|
|
294
337
|
}
|
|
338
|
+
function shouldAutoOpenDashboard() {
|
|
339
|
+
if (process.env.LOUPE_OPEN_BROWSER === '0') {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
return (process.env.NODE_ENV === 'development'
|
|
343
|
+
&& !process.env.CI
|
|
344
|
+
&& !!process.stdout.isTTY);
|
|
345
|
+
}
|
|
346
|
+
function openBrowser(url) {
|
|
347
|
+
const command = process.platform === 'darwin'
|
|
348
|
+
? ['open', [url]]
|
|
349
|
+
: process.platform === 'win32'
|
|
350
|
+
? ['cmd', ['/c', 'start', '', url]]
|
|
351
|
+
: process.platform === 'linux'
|
|
352
|
+
? ['xdg-open', [url]]
|
|
353
|
+
: null;
|
|
354
|
+
if (!command) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const child = childProcess.spawn(command[0], command[1], {
|
|
359
|
+
detached: true,
|
|
360
|
+
stdio: 'ignore',
|
|
361
|
+
});
|
|
362
|
+
child.on('error', () => { });
|
|
363
|
+
child.unref();
|
|
364
|
+
}
|
|
365
|
+
catch (_error) {
|
|
366
|
+
// Ignore browser launch failures. The dashboard URL is already printed.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
295
369
|
function normaliseRequest(request) {
|
|
296
370
|
return {
|
|
297
371
|
input: (0, utils_1.safeClone)(request?.input),
|
package/dist/session-nav.d.ts
CHANGED
|
@@ -41,4 +41,4 @@ export declare function findSessionNodePath(nodes: SessionNavHierarchyNode[], id
|
|
|
41
41
|
export declare function findSessionNodeById(nodes: SessionNavHierarchyNode[], id: string): SessionNavHierarchyNode | null;
|
|
42
42
|
export declare function getNewestTraceIdForNode(node: SessionNavHierarchyNode | null | undefined): string | null;
|
|
43
43
|
export declare function resolveSessionTreeSelection(sessionNodes: SessionNavHierarchyNode[], selectedNodeId: string | null, selectedTraceId: string | null): SessionTreeSelection;
|
|
44
|
-
export declare function getDefaultExpandedSessionTreeNodeIds(sessionNodes: SessionNavHierarchyNode[], activeSessionId: string | null, selectedNodeId: string | null): Set<string>;
|
|
44
|
+
export declare function getDefaultExpandedSessionTreeNodeIds(sessionNodes: SessionNavHierarchyNode[], activeSessionId: string | null, selectedNodeId: string | null, selectedTraceId?: string | null): Set<string>;
|
package/dist/session-nav.js
CHANGED
|
@@ -66,7 +66,7 @@ function resolveSessionTreeSelection(sessionNodes, selectedNodeId, selectedTrace
|
|
|
66
66
|
selectedTraceId: nextSelectedTraceId,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
-
function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId) {
|
|
69
|
+
function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId, selectedTraceId = null) {
|
|
70
70
|
const expanded = new Set();
|
|
71
71
|
const activeSession = (activeSessionId
|
|
72
72
|
? sessionNodes.find((node) => node.id === activeSessionId) ?? null
|
|
@@ -89,6 +89,13 @@ function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, sel
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
if (selectedTraceId) {
|
|
93
|
+
for (const node of findSessionNodePath([activeSession], `trace:${selectedTraceId}`)) {
|
|
94
|
+
if (node.children.length) {
|
|
95
|
+
expanded.add(node.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
92
99
|
return expanded;
|
|
93
100
|
}
|
|
94
101
|
function deriveSessionNavItem(node, traceById) {
|
package/dist/store.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class TraceStore extends EventEmitter {
|
|
|
17
17
|
clear(): void;
|
|
18
18
|
hierarchy(filters?: TraceFilters): HierarchyResponse;
|
|
19
19
|
private recordStart;
|
|
20
|
+
private findTraceBySpanReference;
|
|
20
21
|
private evictIfNeeded;
|
|
21
22
|
private cloneTrace;
|
|
22
23
|
private filteredTraces;
|
package/dist/store.js
CHANGED
|
@@ -141,51 +141,49 @@ class TraceStore extends node_events_1.EventEmitter {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
const roots = new Map();
|
|
144
|
+
const traceBySpanId = new Map();
|
|
145
|
+
const traceNodeByTraceId = new Map();
|
|
146
|
+
const traceSessionByTraceId = new Map();
|
|
147
|
+
const parentNodeById = new Map();
|
|
144
148
|
for (const trace of traces) {
|
|
145
|
-
const sessionId = trace
|
|
149
|
+
const sessionId = getTraceSessionId(trace);
|
|
146
150
|
const sessionNode = getOrCreateNode(roots, `session:${sessionId}`, 'session', `Session ${sessionId}`, {
|
|
147
151
|
sessionId,
|
|
148
152
|
chatId: trace.hierarchy.chatId,
|
|
149
|
-
|
|
150
|
-
const lineage = [sessionNode];
|
|
151
|
-
const rootActorId = trace.hierarchy.rootActorId || 'unknown-actor';
|
|
152
|
-
const actorNode = getOrCreateNode(sessionNode.children, `actor:${sessionId}:${rootActorId}`, 'actor', rootActorId, {
|
|
153
|
-
actorId: rootActorId,
|
|
154
|
-
rootActorId,
|
|
155
|
-
sessionId,
|
|
153
|
+
rootActorId: trace.hierarchy.rootActorId,
|
|
156
154
|
topLevelAgentId: trace.hierarchy.topLevelAgentId,
|
|
157
155
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
else if (trace.hierarchy.childActorId) {
|
|
171
|
-
currentNode = getOrCreateNode(currentNode.children, `child-actor:${sessionId}:${rootActorId}:${trace.hierarchy.childActorId}`, 'child-actor', trace.hierarchy.childActorId, {
|
|
172
|
-
actorId: trace.hierarchy.childActorId,
|
|
173
|
-
childActorId: trace.hierarchy.childActorId,
|
|
174
|
-
delegatedAgentId: trace.hierarchy.delegatedAgentId,
|
|
175
|
-
});
|
|
176
|
-
lineage.push(currentNode);
|
|
156
|
+
const traceNode = createTraceNode(trace);
|
|
157
|
+
traceBySpanId.set(trace.spanContext.spanId, trace);
|
|
158
|
+
traceNodeByTraceId.set(trace.id, traceNode);
|
|
159
|
+
traceSessionByTraceId.set(trace.id, sessionId);
|
|
160
|
+
}
|
|
161
|
+
for (const trace of traces) {
|
|
162
|
+
const sessionId = traceSessionByTraceId.get(trace.id) || 'unknown-session';
|
|
163
|
+
const sessionNode = roots.get(`session:${sessionId}`);
|
|
164
|
+
const traceNode = traceNodeByTraceId.get(trace.id);
|
|
165
|
+
if (!sessionNode || !traceNode) {
|
|
166
|
+
continue;
|
|
177
167
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
168
|
+
const parentTrace = trace.parentSpanId ? traceBySpanId.get(trace.parentSpanId) : null;
|
|
169
|
+
const parentTraceNode = parentTrace &&
|
|
170
|
+
traceSessionByTraceId.get(parentTrace.id) === sessionId &&
|
|
171
|
+
parentTrace.id !== trace.id
|
|
172
|
+
? traceNodeByTraceId.get(parentTrace.id) || null
|
|
173
|
+
: null;
|
|
174
|
+
parentNodeById.set(traceNode.id, parentTraceNode || sessionNode);
|
|
175
|
+
}
|
|
176
|
+
for (const trace of traces) {
|
|
177
|
+
const traceNode = traceNodeByTraceId.get(trace.id);
|
|
178
|
+
const parentNode = traceNode ? parentNodeById.get(traceNode.id) || null : null;
|
|
179
|
+
if (!traceNode || !parentNode) {
|
|
180
|
+
continue;
|
|
184
181
|
}
|
|
185
|
-
|
|
186
|
-
currentNode
|
|
187
|
-
|
|
188
|
-
applyTraceRollup(
|
|
182
|
+
parentNode.children.set(traceNode.id, traceNode);
|
|
183
|
+
let currentNode = parentNode;
|
|
184
|
+
while (currentNode) {
|
|
185
|
+
applyTraceRollup(currentNode, trace);
|
|
186
|
+
currentNode = parentNodeById.get(currentNode.id) || null;
|
|
189
187
|
}
|
|
190
188
|
}
|
|
191
189
|
return {
|
|
@@ -195,9 +193,9 @@ class TraceStore extends node_events_1.EventEmitter {
|
|
|
195
193
|
};
|
|
196
194
|
}
|
|
197
195
|
recordStart(mode, context, request, options = {}) {
|
|
198
|
-
const traceContext = (0, utils_1.normalizeTraceContext)(context, mode);
|
|
196
|
+
const traceContext = applyConversationIdToContext((0, utils_1.normalizeTraceContext)(context, mode), options.attributes);
|
|
199
197
|
const traceId = randomId();
|
|
200
|
-
const parentSpan =
|
|
198
|
+
const parentSpan = this.findTraceBySpanReference(options.parentSpanId);
|
|
201
199
|
const startedAt = new Date().toISOString();
|
|
202
200
|
const trace = {
|
|
203
201
|
attributes: buildSpanAttributes(traceContext, mode, request, options.attributes),
|
|
@@ -253,6 +251,21 @@ class TraceStore extends node_events_1.EventEmitter {
|
|
|
253
251
|
this.publish('span:start', traceId, { trace: this.cloneTrace(trace) });
|
|
254
252
|
return traceId;
|
|
255
253
|
}
|
|
254
|
+
findTraceBySpanReference(spanReference) {
|
|
255
|
+
if (!spanReference) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const byTraceId = this.traces.get(spanReference);
|
|
259
|
+
if (byTraceId) {
|
|
260
|
+
return byTraceId;
|
|
261
|
+
}
|
|
262
|
+
for (const trace of this.traces.values()) {
|
|
263
|
+
if (trace.spanContext.spanId === spanReference) {
|
|
264
|
+
return trace;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
256
269
|
evictIfNeeded() {
|
|
257
270
|
while (this.order.length > this.maxTraces) {
|
|
258
271
|
const oldest = this.order.shift();
|
|
@@ -420,6 +433,35 @@ function buildGroupHierarchy(traces, groupBy) {
|
|
|
420
433
|
}
|
|
421
434
|
return [...groups.values()].map(serialiseNode);
|
|
422
435
|
}
|
|
436
|
+
function getTraceSessionId(trace) {
|
|
437
|
+
const conversationId = toNonEmptyString(trace.attributes?.['gen_ai.conversation.id']);
|
|
438
|
+
return conversationId || trace.hierarchy.sessionId || 'unknown-session';
|
|
439
|
+
}
|
|
440
|
+
function applyConversationIdToContext(context, extraAttributes) {
|
|
441
|
+
const conversationId = toNonEmptyString(extraAttributes?.['gen_ai.conversation.id']);
|
|
442
|
+
if (!conversationId || conversationId === context.sessionId) {
|
|
443
|
+
return context;
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
...context,
|
|
447
|
+
sessionId: conversationId,
|
|
448
|
+
chatId: conversationId,
|
|
449
|
+
rootSessionId: context.rootSessionId || conversationId,
|
|
450
|
+
rootChatId: context.rootChatId || conversationId,
|
|
451
|
+
tags: {
|
|
452
|
+
...context.tags,
|
|
453
|
+
sessionId: conversationId,
|
|
454
|
+
chatId: conversationId,
|
|
455
|
+
rootSessionId: context.rootSessionId || conversationId,
|
|
456
|
+
rootChatId: context.rootChatId || conversationId,
|
|
457
|
+
},
|
|
458
|
+
hierarchy: {
|
|
459
|
+
...context.hierarchy,
|
|
460
|
+
sessionId: conversationId,
|
|
461
|
+
chatId: conversationId,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
423
465
|
function randomId() {
|
|
424
466
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
425
467
|
}
|
|
@@ -469,6 +511,9 @@ function getDefaultSpanName(context, mode) {
|
|
|
469
511
|
const prefix = context.provider || 'llm';
|
|
470
512
|
return `${prefix}.${mode}`;
|
|
471
513
|
}
|
|
514
|
+
function toNonEmptyString(value) {
|
|
515
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
516
|
+
}
|
|
472
517
|
function buildSpanAttributes(context, mode, request, extraAttributes) {
|
|
473
518
|
const base = {
|
|
474
519
|
'gen_ai.conversation.id': context.sessionId || undefined,
|
package/dist/utils.js
CHANGED
|
@@ -134,7 +134,9 @@ function normalizeTraceContext(context, mode) {
|
|
|
134
134
|
const explicitKind = normalizeKind(raw.kind);
|
|
135
135
|
let kind = explicitKind || 'actor';
|
|
136
136
|
if (!explicitKind) {
|
|
137
|
-
|
|
137
|
+
// Keep generic guardrail metadata from masking actual delegated-agent spans.
|
|
138
|
+
// Explicit guardrail spans or spans with a concrete phase still remain guardrails.
|
|
139
|
+
if (guardrailPhase || (guardrailType && !isChildActor)) {
|
|
138
140
|
kind = 'guardrail';
|
|
139
141
|
}
|
|
140
142
|
else if (stage) {
|