@mtharrison/loupe 1.1.0 → 1.2.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 +103 -12
- package/assets/screenshot1.png +0 -0
- package/assets/screenshot2.png +0 -0
- package/dist/client/app.css +856 -233
- package/dist/client/app.js +3842 -18526
- package/dist/client/chunk-FF2MKFR7.js +1318 -0
- package/dist/client/markdown-block-DMQHS3E5.js +14377 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +330 -2
- package/dist/server.d.ts +1 -0
- package/dist/server.js +42 -11
- package/dist/session-nav.d.ts +44 -0
- package/dist/session-nav.js +223 -0
- package/dist/store.js +4 -1
- package/dist/types.d.ts +41 -0
- package/dist/ui-build.js +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +173 -0
- package/examples/openai-multiturn-tools.js +399 -0
- package/package.json +3 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveSessionNavItems = deriveSessionNavItems;
|
|
4
|
+
exports.sortSessionNodesForNav = sortSessionNodesForNav;
|
|
5
|
+
exports.findSessionNodePath = findSessionNodePath;
|
|
6
|
+
exports.findSessionNodeById = findSessionNodeById;
|
|
7
|
+
exports.getNewestTraceIdForNode = getNewestTraceIdForNode;
|
|
8
|
+
exports.resolveSessionTreeSelection = resolveSessionTreeSelection;
|
|
9
|
+
exports.getDefaultExpandedSessionTreeNodeIds = getDefaultExpandedSessionTreeNodeIds;
|
|
10
|
+
function deriveSessionNavItems(sessionNodes, traceById) {
|
|
11
|
+
return sessionNodes
|
|
12
|
+
.map((node) => deriveSessionNavItem(node, traceById))
|
|
13
|
+
.sort(compareSessionNavItems);
|
|
14
|
+
}
|
|
15
|
+
function sortSessionNodesForNav(sessionNodes, traceById) {
|
|
16
|
+
const itemById = new Map(sessionNodes.map((node) => [node.id, deriveSessionNavItem(node, traceById)]));
|
|
17
|
+
return sessionNodes
|
|
18
|
+
.slice()
|
|
19
|
+
.sort((left, right) => compareSessionNavItems(itemById.get(left.id), itemById.get(right.id)));
|
|
20
|
+
}
|
|
21
|
+
function findSessionNodePath(nodes, id, trail = []) {
|
|
22
|
+
for (const node of nodes) {
|
|
23
|
+
const nextTrail = [...trail, node];
|
|
24
|
+
if (node.id === id) {
|
|
25
|
+
return nextTrail;
|
|
26
|
+
}
|
|
27
|
+
const childTrail = findSessionNodePath(node.children, id, nextTrail);
|
|
28
|
+
if (childTrail.length) {
|
|
29
|
+
return childTrail;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
function findSessionNodeById(nodes, id) {
|
|
35
|
+
return findSessionNodePath(nodes, id).at(-1) ?? null;
|
|
36
|
+
}
|
|
37
|
+
function getNewestTraceIdForNode(node) {
|
|
38
|
+
if (!node?.traceIds.length) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (typeof node.meta?.traceId === "string" && node.meta.traceId) {
|
|
42
|
+
return node.meta.traceId;
|
|
43
|
+
}
|
|
44
|
+
return node.traceIds[0] || null;
|
|
45
|
+
}
|
|
46
|
+
function resolveSessionTreeSelection(sessionNodes, selectedNodeId, selectedTraceId) {
|
|
47
|
+
const selectedNode = selectedNodeId
|
|
48
|
+
? findSessionNodeById(sessionNodes, selectedNodeId)
|
|
49
|
+
: null;
|
|
50
|
+
const selectedTraceNode = selectedTraceId
|
|
51
|
+
? findSessionNodeById(sessionNodes, `trace:${selectedTraceId}`)
|
|
52
|
+
: null;
|
|
53
|
+
const fallbackNode = selectedNode ?? selectedTraceNode ?? sessionNodes[0] ?? null;
|
|
54
|
+
if (!fallbackNode) {
|
|
55
|
+
return {
|
|
56
|
+
selectedNodeId: null,
|
|
57
|
+
selectedTraceId: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const nextSelectedNodeId = selectedNode?.id ?? fallbackNode.id;
|
|
61
|
+
const nextSelectedTraceId = selectedTraceId && fallbackNode.traceIds.includes(selectedTraceId)
|
|
62
|
+
? selectedTraceId
|
|
63
|
+
: getNewestTraceIdForNode(fallbackNode);
|
|
64
|
+
return {
|
|
65
|
+
selectedNodeId: nextSelectedNodeId,
|
|
66
|
+
selectedTraceId: nextSelectedTraceId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId) {
|
|
70
|
+
const expanded = new Set();
|
|
71
|
+
const activeSession = (activeSessionId
|
|
72
|
+
? sessionNodes.find((node) => node.id === activeSessionId) ?? null
|
|
73
|
+
: null) ?? sessionNodes[0] ?? null;
|
|
74
|
+
if (!activeSession) {
|
|
75
|
+
return expanded;
|
|
76
|
+
}
|
|
77
|
+
if (activeSession.children.length) {
|
|
78
|
+
expanded.add(activeSession.id);
|
|
79
|
+
}
|
|
80
|
+
visitSessionTree(activeSession.children, (node) => {
|
|
81
|
+
if (node.children.length && node.type === "actor") {
|
|
82
|
+
expanded.add(node.id);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (selectedNodeId) {
|
|
86
|
+
for (const node of findSessionNodePath([activeSession], selectedNodeId)) {
|
|
87
|
+
if (node.children.length) {
|
|
88
|
+
expanded.add(node.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return expanded;
|
|
93
|
+
}
|
|
94
|
+
function deriveSessionNavItem(node, traceById) {
|
|
95
|
+
const traces = node.traceIds
|
|
96
|
+
.map((traceId) => traceById.get(traceId))
|
|
97
|
+
.filter((trace) => Boolean(trace));
|
|
98
|
+
const latestTrace = getLatestTrace(traces);
|
|
99
|
+
const sessionId = getSessionId(node) ?? latestTrace?.hierarchy.sessionId ?? "unknown";
|
|
100
|
+
const shortSessionId = shortId(sessionId);
|
|
101
|
+
const primaryActor = getPrimaryActor(node) ?? latestTrace?.hierarchy.rootActorId ?? shortSessionId;
|
|
102
|
+
return {
|
|
103
|
+
callCount: node.count,
|
|
104
|
+
costUsd: getCostUsd(node, traces),
|
|
105
|
+
hasHighlights: traces.some((trace) => Boolean(trace.flags?.hasHighlights)),
|
|
106
|
+
id: node.id,
|
|
107
|
+
latestStartedAt: latestTrace?.startedAt ?? null,
|
|
108
|
+
latestTimestamp: latestTrace?.startedAt
|
|
109
|
+
? formatCompactTimestamp(latestTrace.startedAt)
|
|
110
|
+
: null,
|
|
111
|
+
primaryActor,
|
|
112
|
+
primaryLabel: primaryActor,
|
|
113
|
+
shortSessionId,
|
|
114
|
+
status: getAggregateStatus(traces),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function getCostUsd(node, traces) {
|
|
118
|
+
if (typeof node.meta?.costUsd === "number" &&
|
|
119
|
+
Number.isFinite(node.meta.costUsd)) {
|
|
120
|
+
return roundCostUsd(node.meta.costUsd);
|
|
121
|
+
}
|
|
122
|
+
const traceCosts = traces
|
|
123
|
+
.map((trace) => trace.costUsd)
|
|
124
|
+
.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
125
|
+
if (!traceCosts.length) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return roundCostUsd(traceCosts.reduce((sum, value) => sum + value, 0));
|
|
129
|
+
}
|
|
130
|
+
function getLatestTrace(traces) {
|
|
131
|
+
let latestTrace = null;
|
|
132
|
+
let latestTimestamp = Number.NEGATIVE_INFINITY;
|
|
133
|
+
for (const trace of traces) {
|
|
134
|
+
const nextTimestamp = Date.parse(trace.startedAt);
|
|
135
|
+
if (!Number.isFinite(nextTimestamp)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (nextTimestamp > latestTimestamp) {
|
|
139
|
+
latestTrace = trace;
|
|
140
|
+
latestTimestamp = nextTimestamp;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return latestTrace;
|
|
144
|
+
}
|
|
145
|
+
function getAggregateStatus(traces) {
|
|
146
|
+
if (traces.some((trace) => trace.status === "error")) {
|
|
147
|
+
return "error";
|
|
148
|
+
}
|
|
149
|
+
if (traces.some((trace) => trace.status === "pending")) {
|
|
150
|
+
return "pending";
|
|
151
|
+
}
|
|
152
|
+
return "ok";
|
|
153
|
+
}
|
|
154
|
+
function getSessionId(node) {
|
|
155
|
+
if (typeof node.meta?.sessionId === "string" && node.meta.sessionId) {
|
|
156
|
+
return node.meta.sessionId;
|
|
157
|
+
}
|
|
158
|
+
if (node.id.startsWith("session:")) {
|
|
159
|
+
return node.id.slice("session:".length);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function getPrimaryActor(node) {
|
|
164
|
+
const actorNode = node.children.find((child) => child.type === "actor");
|
|
165
|
+
return (actorNode?.meta?.actorId ??
|
|
166
|
+
actorNode?.meta?.rootActorId ??
|
|
167
|
+
node.meta?.rootActorId ??
|
|
168
|
+
null);
|
|
169
|
+
}
|
|
170
|
+
function compareSessionNavItems(left, right) {
|
|
171
|
+
const timestampDelta = toSortableTimestamp(right.latestStartedAt) -
|
|
172
|
+
toSortableTimestamp(left.latestStartedAt);
|
|
173
|
+
if (timestampDelta !== 0) {
|
|
174
|
+
return timestampDelta;
|
|
175
|
+
}
|
|
176
|
+
const statusDelta = getStatusRank(left.status) - getStatusRank(right.status);
|
|
177
|
+
if (statusDelta !== 0) {
|
|
178
|
+
return statusDelta;
|
|
179
|
+
}
|
|
180
|
+
const costDelta = (right.costUsd ?? 0) - (left.costUsd ?? 0);
|
|
181
|
+
if (costDelta !== 0) {
|
|
182
|
+
return costDelta;
|
|
183
|
+
}
|
|
184
|
+
return left.primaryLabel.localeCompare(right.primaryLabel);
|
|
185
|
+
}
|
|
186
|
+
function getStatusRank(status) {
|
|
187
|
+
switch (status) {
|
|
188
|
+
case "error":
|
|
189
|
+
return 0;
|
|
190
|
+
case "pending":
|
|
191
|
+
return 1;
|
|
192
|
+
default:
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function toSortableTimestamp(value) {
|
|
197
|
+
if (!value) {
|
|
198
|
+
return Number.NEGATIVE_INFINITY;
|
|
199
|
+
}
|
|
200
|
+
const timestamp = Date.parse(value);
|
|
201
|
+
return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
|
|
202
|
+
}
|
|
203
|
+
function roundCostUsd(value) {
|
|
204
|
+
return Math.round(value * 1e12) / 1e12;
|
|
205
|
+
}
|
|
206
|
+
function shortId(value) {
|
|
207
|
+
if (!value) {
|
|
208
|
+
return "unknown";
|
|
209
|
+
}
|
|
210
|
+
return value.length > 8 ? value.slice(0, 8) : value;
|
|
211
|
+
}
|
|
212
|
+
function formatCompactTimestamp(value) {
|
|
213
|
+
return new Date(value).toLocaleTimeString([], {
|
|
214
|
+
hour: "2-digit",
|
|
215
|
+
minute: "2-digit",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function visitSessionTree(nodes, visitor) {
|
|
219
|
+
for (const node of nodes) {
|
|
220
|
+
visitor(node);
|
|
221
|
+
visitSessionTree(node.children, visitor);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/dist/store.js
CHANGED
|
@@ -220,7 +220,10 @@ class TraceStore extends node_events_1.EventEmitter {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
cloneTrace(trace) {
|
|
223
|
-
return
|
|
223
|
+
return {
|
|
224
|
+
...(0, utils_1.safeClone)(trace),
|
|
225
|
+
insights: (0, utils_1.getTraceInsights)(trace),
|
|
226
|
+
};
|
|
224
227
|
}
|
|
225
228
|
filteredTraces(filters = {}) {
|
|
226
229
|
const tagFilters = normalizeTagFilters(filters.tags || filters.tagFilters);
|
package/dist/types.d.ts
CHANGED
|
@@ -79,6 +79,27 @@ export type TraceRequest = {
|
|
|
79
79
|
input?: Record<string, any>;
|
|
80
80
|
options?: Record<string, any>;
|
|
81
81
|
};
|
|
82
|
+
export type TraceStructuredInputInsight = {
|
|
83
|
+
format: 'xml';
|
|
84
|
+
role: string;
|
|
85
|
+
snippet: string;
|
|
86
|
+
tags: string[];
|
|
87
|
+
};
|
|
88
|
+
export type TraceHighlightInsight = {
|
|
89
|
+
description: string;
|
|
90
|
+
kind: string;
|
|
91
|
+
snippet: string;
|
|
92
|
+
source: string;
|
|
93
|
+
title: string;
|
|
94
|
+
};
|
|
95
|
+
export type TraceInsights = {
|
|
96
|
+
highlights: TraceHighlightInsight[];
|
|
97
|
+
structuredInputs: TraceStructuredInputInsight[];
|
|
98
|
+
};
|
|
99
|
+
export type TraceSummaryFlags = {
|
|
100
|
+
hasHighlights: boolean;
|
|
101
|
+
hasStructuredInput: boolean;
|
|
102
|
+
};
|
|
82
103
|
export type TraceRecord = {
|
|
83
104
|
context: NormalizedTraceContext;
|
|
84
105
|
endedAt: string | null;
|
|
@@ -106,6 +127,7 @@ export type TraceRecord = {
|
|
|
106
127
|
usage: Record<string, any> | null;
|
|
107
128
|
};
|
|
108
129
|
};
|
|
130
|
+
insights?: TraceInsights;
|
|
109
131
|
tags: TraceTags;
|
|
110
132
|
usage: Record<string, any> | null;
|
|
111
133
|
};
|
|
@@ -113,6 +135,7 @@ export type TraceSummary = {
|
|
|
113
135
|
costUsd: number | null;
|
|
114
136
|
durationMs: number | null;
|
|
115
137
|
endedAt: string | null;
|
|
138
|
+
flags?: TraceSummaryFlags;
|
|
116
139
|
hierarchy: TraceHierarchy;
|
|
117
140
|
id: string;
|
|
118
141
|
kind: string;
|
|
@@ -179,6 +202,24 @@ export interface ChatModelLike<TInput = any, TOptions = any, TValue = any, TChun
|
|
|
179
202
|
invoke(input: TInput, options?: TOptions): Promise<TValue>;
|
|
180
203
|
stream(input: TInput, options?: TOptions): AsyncGenerator<TChunk>;
|
|
181
204
|
}
|
|
205
|
+
export type OpenAIChatCompletionCreateParamsLike = Record<string, any> & {
|
|
206
|
+
messages?: Record<string, any>[];
|
|
207
|
+
model?: string | null;
|
|
208
|
+
stream?: boolean | null;
|
|
209
|
+
};
|
|
210
|
+
export interface OpenAIChatCompletionStreamLike<TChunk = any> extends AsyncIterable<TChunk> {
|
|
211
|
+
[Symbol.asyncIterator](): AsyncIterator<TChunk>;
|
|
212
|
+
}
|
|
213
|
+
export interface OpenAIChatCompletionsLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
|
|
214
|
+
create(params: TParams, options?: TOptions): Promise<TResponse> | Promise<OpenAIChatCompletionStreamLike<TChunk>> | OpenAIChatCompletionStreamLike<TChunk>;
|
|
215
|
+
}
|
|
216
|
+
export interface OpenAIClientLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
|
|
217
|
+
chat: {
|
|
218
|
+
completions: OpenAIChatCompletionsLike<TParams, TOptions, TResponse, TChunk>;
|
|
219
|
+
[key: string]: any;
|
|
220
|
+
};
|
|
221
|
+
[key: string]: any;
|
|
222
|
+
}
|
|
182
223
|
export type TraceServer = {
|
|
183
224
|
broadcast(event: TraceEvent | UIReloadEvent): void;
|
|
184
225
|
close(): void;
|
package/dist/ui-build.js
CHANGED
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NormalizedTraceContext, type TraceContext, type TraceMode, type TraceRecord, type TraceSummary } from './types';
|
|
1
|
+
import { type NormalizedTraceContext, type TraceContext, type TraceInsights, type TraceMode, type TraceRecord, type TraceSummary } from './types';
|
|
2
2
|
export declare function safeClone<T>(value: T): T;
|
|
3
3
|
export declare function toErrorPayload(error: any): Record<string, any> | null;
|
|
4
4
|
export declare function sanitizeHeaders(headers: Record<string, any> | undefined): Record<string, any>;
|
|
@@ -7,4 +7,5 @@ export declare function stringifyRecord(record: Record<string, any>): Record<str
|
|
|
7
7
|
export declare function normalizeTraceContext(context: TraceContext | undefined, mode: TraceMode): NormalizedTraceContext;
|
|
8
8
|
export declare function summariseValue(value: unknown, maxLength?: number): string;
|
|
9
9
|
export declare function getUsageCostUsd(usage: Record<string, any> | null | undefined): number | null;
|
|
10
|
+
export declare function getTraceInsights(trace: TraceRecord): TraceInsights;
|
|
10
11
|
export declare function toSummary(trace: TraceRecord): TraceSummary;
|
package/dist/utils.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.stringifyRecord = stringifyRecord;
|
|
|
8
8
|
exports.normalizeTraceContext = normalizeTraceContext;
|
|
9
9
|
exports.summariseValue = summariseValue;
|
|
10
10
|
exports.getUsageCostUsd = getUsageCostUsd;
|
|
11
|
+
exports.getTraceInsights = getTraceInsights;
|
|
11
12
|
exports.toSummary = toSummary;
|
|
12
13
|
function safeClone(value) {
|
|
13
14
|
if (value === undefined) {
|
|
@@ -253,12 +254,84 @@ function extractResponsePreview(trace) {
|
|
|
253
254
|
}
|
|
254
255
|
return '';
|
|
255
256
|
}
|
|
257
|
+
function getTraceInsights(trace) {
|
|
258
|
+
const structuredInputs = [];
|
|
259
|
+
const highlights = [];
|
|
260
|
+
const seenStructured = new Set();
|
|
261
|
+
const seenHighlights = new Set();
|
|
262
|
+
for (const message of collectTraceMessages(trace)) {
|
|
263
|
+
const text = toInsightText(message.content);
|
|
264
|
+
if (!text) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const structuredMarkup = detectStructuredMarkup(text);
|
|
268
|
+
if (structuredMarkup) {
|
|
269
|
+
const structuredKey = `${message.source}:${structuredMarkup.tags.join('|')}:${structuredMarkup.snippet}`;
|
|
270
|
+
if (!seenStructured.has(structuredKey)) {
|
|
271
|
+
structuredInputs.push({
|
|
272
|
+
format: 'xml',
|
|
273
|
+
role: message.role,
|
|
274
|
+
snippet: structuredMarkup.snippet,
|
|
275
|
+
tags: structuredMarkup.tags,
|
|
276
|
+
});
|
|
277
|
+
seenStructured.add(structuredKey);
|
|
278
|
+
}
|
|
279
|
+
const highlightKey = `structured:${message.source}:${structuredMarkup.tags.join('|')}`;
|
|
280
|
+
if (!seenHighlights.has(highlightKey)) {
|
|
281
|
+
highlights.push({
|
|
282
|
+
kind: 'structured-input',
|
|
283
|
+
title: `Structured ${message.role} input`,
|
|
284
|
+
description: `Contains XML-like markup (${structuredMarkup.tags.slice(0, 3).join(', ')}) that may influence guardrail behavior.`,
|
|
285
|
+
source: message.source,
|
|
286
|
+
snippet: structuredMarkup.snippet,
|
|
287
|
+
});
|
|
288
|
+
seenHighlights.add(highlightKey);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if ((message.role === 'user' || message.role === 'system') && looksLikeLongPrompt(text)) {
|
|
292
|
+
const longKey = `long:${message.source}`;
|
|
293
|
+
if (!seenHighlights.has(longKey)) {
|
|
294
|
+
highlights.push({
|
|
295
|
+
kind: 'long-message',
|
|
296
|
+
title: `Long ${message.role} message`,
|
|
297
|
+
description: 'Large prompt payloads often hide embedded instructions, policies, or contextual data worth inspecting.',
|
|
298
|
+
source: message.source,
|
|
299
|
+
snippet: createSnippet(text, 240),
|
|
300
|
+
});
|
|
301
|
+
seenHighlights.add(longKey);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (trace.kind === 'guardrail') {
|
|
306
|
+
const contextSummary = [trace.context.guardrailPhase, trace.context.guardrailType, trace.context.systemType]
|
|
307
|
+
.filter(Boolean)
|
|
308
|
+
.join(' / ');
|
|
309
|
+
if (contextSummary) {
|
|
310
|
+
highlights.push({
|
|
311
|
+
kind: 'guardrail-context',
|
|
312
|
+
title: `${capitalize(trace.context.guardrailPhase || 'guardrail')} guardrail context`,
|
|
313
|
+
description: `This trace ran inside ${contextSummary}.`,
|
|
314
|
+
source: 'trace:guardrail',
|
|
315
|
+
snippet: createSnippet(contextSummary, 180),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
structuredInputs,
|
|
321
|
+
highlights,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
256
324
|
function toSummary(trace) {
|
|
257
325
|
const durationMs = trace.endedAt ? Math.max(0, Date.parse(trace.endedAt) - Date.parse(trace.startedAt)) : null;
|
|
326
|
+
const insights = trace.insights || getTraceInsights(trace);
|
|
258
327
|
return {
|
|
259
328
|
costUsd: getUsageCostUsd(trace.usage),
|
|
260
329
|
durationMs,
|
|
261
330
|
endedAt: trace.endedAt,
|
|
331
|
+
flags: {
|
|
332
|
+
hasHighlights: insights.highlights.length > 0,
|
|
333
|
+
hasStructuredInput: insights.structuredInputs.length > 0,
|
|
334
|
+
},
|
|
262
335
|
hierarchy: safeClone(trace.hierarchy),
|
|
263
336
|
id: trace.id,
|
|
264
337
|
kind: trace.kind,
|
|
@@ -278,3 +351,103 @@ function toSummary(trace) {
|
|
|
278
351
|
tags: safeClone(trace.tags),
|
|
279
352
|
};
|
|
280
353
|
}
|
|
354
|
+
function collectTraceMessages(trace) {
|
|
355
|
+
const entries = [];
|
|
356
|
+
const requestMessages = trace.request?.input?.messages;
|
|
357
|
+
if (Array.isArray(requestMessages)) {
|
|
358
|
+
for (const [index, message] of requestMessages.entries()) {
|
|
359
|
+
entries.push({
|
|
360
|
+
content: message?.content,
|
|
361
|
+
role: typeof message?.role === 'string' ? message.role : 'unknown',
|
|
362
|
+
source: `request:${index}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const responseMessage = trace.response?.message || trace.stream?.reconstructed?.message;
|
|
367
|
+
if (responseMessage?.content !== undefined) {
|
|
368
|
+
entries.push({
|
|
369
|
+
content: responseMessage.content,
|
|
370
|
+
role: typeof responseMessage.role === 'string' ? responseMessage.role : 'assistant',
|
|
371
|
+
source: 'response:0',
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return entries;
|
|
375
|
+
}
|
|
376
|
+
function toInsightText(content) {
|
|
377
|
+
if (typeof content === 'string') {
|
|
378
|
+
const trimmed = content.trim();
|
|
379
|
+
return trimmed ? trimmed : null;
|
|
380
|
+
}
|
|
381
|
+
if (Array.isArray(content)) {
|
|
382
|
+
const parts = content
|
|
383
|
+
.map((item) => {
|
|
384
|
+
if (typeof item === 'string') {
|
|
385
|
+
return item;
|
|
386
|
+
}
|
|
387
|
+
if (typeof item?.text === 'string') {
|
|
388
|
+
return item.text;
|
|
389
|
+
}
|
|
390
|
+
if (typeof item?.text?.value === 'string') {
|
|
391
|
+
return item.text.value;
|
|
392
|
+
}
|
|
393
|
+
if (typeof item?.content === 'string') {
|
|
394
|
+
return item.content;
|
|
395
|
+
}
|
|
396
|
+
return '';
|
|
397
|
+
})
|
|
398
|
+
.filter(Boolean);
|
|
399
|
+
return parts.length ? parts.join('\n\n').trim() : null;
|
|
400
|
+
}
|
|
401
|
+
if (typeof content?.text === 'string') {
|
|
402
|
+
return content.text.trim() || null;
|
|
403
|
+
}
|
|
404
|
+
if (typeof content?.text?.value === 'string') {
|
|
405
|
+
return content.text.value.trim() || null;
|
|
406
|
+
}
|
|
407
|
+
if (typeof content?.content === 'string') {
|
|
408
|
+
return content.content.trim() || null;
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function detectStructuredMarkup(text) {
|
|
413
|
+
const tagRegex = /<\/?([A-Za-z][\w:-]*)\b[^>]*>/g;
|
|
414
|
+
const tagNames = [];
|
|
415
|
+
let match = tagRegex.exec(text);
|
|
416
|
+
while (match) {
|
|
417
|
+
tagNames.push(match[1].toLowerCase());
|
|
418
|
+
match = tagRegex.exec(text);
|
|
419
|
+
}
|
|
420
|
+
const uniqueTags = [...new Set(tagNames)];
|
|
421
|
+
if (!uniqueTags.length) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const hasInstructionalTag = uniqueTags.some((tag) => ['assistant', 'instruction', 'option', 'policy', 'system', 'user'].includes(tag));
|
|
425
|
+
const hasMultipleTags = uniqueTags.length >= 2;
|
|
426
|
+
const hasClosingTag = /<\/[A-Za-z][\w:-]*>/.test(text);
|
|
427
|
+
if (!hasClosingTag && !hasInstructionalTag && !hasMultipleTags) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
snippet: createSnippet(text, 220),
|
|
432
|
+
tags: uniqueTags.slice(0, 6),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function looksLikeLongPrompt(text) {
|
|
436
|
+
return text.length >= 1200 || countLines(text) >= 20;
|
|
437
|
+
}
|
|
438
|
+
function countLines(text) {
|
|
439
|
+
return text.split(/\r?\n/).length;
|
|
440
|
+
}
|
|
441
|
+
function createSnippet(text, maxLength) {
|
|
442
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
443
|
+
if (trimmed.length <= maxLength) {
|
|
444
|
+
return trimmed;
|
|
445
|
+
}
|
|
446
|
+
return `${trimmed.slice(0, maxLength - 1)}...`;
|
|
447
|
+
}
|
|
448
|
+
function capitalize(value) {
|
|
449
|
+
if (!value) {
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
452
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
453
|
+
}
|