langwatch 0.0.1
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/.eslintrc.cjs +37 -0
- package/README.md +3 -0
- package/dist/chunk-GOA2HL4A.mjs +269 -0
- package/dist/chunk-GOA2HL4A.mjs.map +1 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +940 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +666 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils-s3gGR6vj.d.mts +209 -0
- package/dist/utils-s3gGR6vj.d.ts +209 -0
- package/dist/utils.d.mts +3 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +263 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +7 -0
- package/dist/utils.mjs.map +1 -0
- package/example/.env.example +12 -0
- package/example/.eslintrc.json +26 -0
- package/example/LICENSE +13 -0
- package/example/README.md +10 -0
- package/example/app/(chat)/chat/[id]/page.tsx +60 -0
- package/example/app/(chat)/layout.tsx +14 -0
- package/example/app/(chat)/page.tsx +22 -0
- package/example/app/actions.ts +156 -0
- package/example/app/globals.css +76 -0
- package/example/app/layout.tsx +64 -0
- package/example/app/login/actions.ts +71 -0
- package/example/app/login/page.tsx +18 -0
- package/example/app/new/page.tsx +5 -0
- package/example/app/opengraph-image.png +0 -0
- package/example/app/share/[id]/page.tsx +58 -0
- package/example/app/signup/actions.ts +111 -0
- package/example/app/signup/page.tsx +18 -0
- package/example/app/twitter-image.png +0 -0
- package/example/auth.config.ts +42 -0
- package/example/auth.ts +45 -0
- package/example/components/button-scroll-to-bottom.tsx +36 -0
- package/example/components/chat-history.tsx +49 -0
- package/example/components/chat-list.tsx +52 -0
- package/example/components/chat-message-actions.tsx +40 -0
- package/example/components/chat-message.tsx +80 -0
- package/example/components/chat-panel.tsx +139 -0
- package/example/components/chat-share-dialog.tsx +95 -0
- package/example/components/chat.tsx +84 -0
- package/example/components/clear-history.tsx +75 -0
- package/example/components/empty-screen.tsx +38 -0
- package/example/components/external-link.tsx +29 -0
- package/example/components/footer.tsx +19 -0
- package/example/components/header.tsx +80 -0
- package/example/components/login-button.tsx +42 -0
- package/example/components/login-form.tsx +97 -0
- package/example/components/markdown.tsx +9 -0
- package/example/components/prompt-form.tsx +115 -0
- package/example/components/providers.tsx +17 -0
- package/example/components/sidebar-actions.tsx +125 -0
- package/example/components/sidebar-desktop.tsx +19 -0
- package/example/components/sidebar-footer.tsx +16 -0
- package/example/components/sidebar-item.tsx +124 -0
- package/example/components/sidebar-items.tsx +42 -0
- package/example/components/sidebar-list.tsx +38 -0
- package/example/components/sidebar-mobile.tsx +31 -0
- package/example/components/sidebar-toggle.tsx +24 -0
- package/example/components/sidebar.tsx +21 -0
- package/example/components/signup-form.tsx +95 -0
- package/example/components/stocks/events-skeleton.tsx +31 -0
- package/example/components/stocks/events.tsx +30 -0
- package/example/components/stocks/index.tsx +36 -0
- package/example/components/stocks/message.tsx +134 -0
- package/example/components/stocks/spinner.tsx +16 -0
- package/example/components/stocks/stock-purchase.tsx +146 -0
- package/example/components/stocks/stock-skeleton.tsx +22 -0
- package/example/components/stocks/stock.tsx +210 -0
- package/example/components/stocks/stocks-skeleton.tsx +9 -0
- package/example/components/stocks/stocks.tsx +67 -0
- package/example/components/tailwind-indicator.tsx +14 -0
- package/example/components/theme-toggle.tsx +31 -0
- package/example/components/ui/alert-dialog.tsx +141 -0
- package/example/components/ui/badge.tsx +36 -0
- package/example/components/ui/button.tsx +57 -0
- package/example/components/ui/codeblock.tsx +148 -0
- package/example/components/ui/dialog.tsx +122 -0
- package/example/components/ui/dropdown-menu.tsx +205 -0
- package/example/components/ui/icons.tsx +507 -0
- package/example/components/ui/input.tsx +25 -0
- package/example/components/ui/label.tsx +26 -0
- package/example/components/ui/select.tsx +164 -0
- package/example/components/ui/separator.tsx +31 -0
- package/example/components/ui/sheet.tsx +140 -0
- package/example/components/ui/sonner.tsx +31 -0
- package/example/components/ui/switch.tsx +29 -0
- package/example/components/ui/textarea.tsx +24 -0
- package/example/components/ui/tooltip.tsx +30 -0
- package/example/components/user-menu.tsx +53 -0
- package/example/components.json +17 -0
- package/example/lib/chat/actions.tsx +606 -0
- package/example/lib/hooks/use-copy-to-clipboard.tsx +33 -0
- package/example/lib/hooks/use-enter-submit.tsx +23 -0
- package/example/lib/hooks/use-local-storage.ts +24 -0
- package/example/lib/hooks/use-scroll-anchor.tsx +86 -0
- package/example/lib/hooks/use-sidebar.tsx +60 -0
- package/example/lib/hooks/use-streamable-text.ts +25 -0
- package/example/lib/types.ts +41 -0
- package/example/lib/utils.ts +89 -0
- package/example/middleware.ts +8 -0
- package/example/next-env.d.ts +5 -0
- package/example/next.config.js +13 -0
- package/example/package-lock.json +9249 -0
- package/example/package.json +77 -0
- package/example/pnpm-lock.yaml +5712 -0
- package/example/postcss.config.js +6 -0
- package/example/prettier.config.cjs +34 -0
- package/example/public/apple-touch-icon.png +0 -0
- package/example/public/favicon-16x16.png +0 -0
- package/example/public/favicon.ico +0 -0
- package/example/public/next.svg +1 -0
- package/example/public/thirteen.svg +1 -0
- package/example/public/vercel.svg +1 -0
- package/example/tailwind.config.ts +81 -0
- package/example/tsconfig.json +35 -0
- package/package.json +45 -0
- package/src/helpers.ts +64 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +397 -0
- package/src/server/types/.gitkeep +0 -0
- package/src/types.ts +69 -0
- package/src/utils.ts +134 -0
- package/ts-to-zod.config.js +18 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import EventEmitter from "events";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { ZodError } from "zod";
|
|
4
|
+
import { fromZodError } from "zod-validation-error";
|
|
5
|
+
import { camelToSnakeCaseNested, type Strict } from "./helpers";
|
|
6
|
+
import {
|
|
7
|
+
type CollectorRESTParams,
|
|
8
|
+
type Span as ServerSpan,
|
|
9
|
+
type SpanTypes,
|
|
10
|
+
} from "./server/types/tracer";
|
|
11
|
+
import {
|
|
12
|
+
collectorRESTParamsSchema,
|
|
13
|
+
spanSchema,
|
|
14
|
+
} from "./server/types/tracer.generated";
|
|
15
|
+
import {
|
|
16
|
+
type BaseSpan,
|
|
17
|
+
type ChatMessage,
|
|
18
|
+
type ChatRichContent,
|
|
19
|
+
type LLMSpan,
|
|
20
|
+
type Metadata,
|
|
21
|
+
type PendingBaseSpan,
|
|
22
|
+
type PendingLLMSpan,
|
|
23
|
+
type PendingRAGSpan,
|
|
24
|
+
type RAGSpan,
|
|
25
|
+
type SpanInputOutput,
|
|
26
|
+
} from "./types";
|
|
27
|
+
import { convertFromVercelAIMessages } from "./utils";
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
BaseSpan,
|
|
31
|
+
ChatMessage as ChatMessage,
|
|
32
|
+
ChatRichContent,
|
|
33
|
+
LLMSpan,
|
|
34
|
+
Metadata,
|
|
35
|
+
PendingBaseSpan,
|
|
36
|
+
PendingLLMSpan,
|
|
37
|
+
PendingRAGSpan,
|
|
38
|
+
RAGSpan,
|
|
39
|
+
SpanInputOutput,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { convertFromVercelAIMessages };
|
|
43
|
+
|
|
44
|
+
export class LangWatch extends EventEmitter {
|
|
45
|
+
apiKey: string | undefined;
|
|
46
|
+
endpoint: string;
|
|
47
|
+
|
|
48
|
+
constructor({
|
|
49
|
+
apiKey,
|
|
50
|
+
endpoint = process.env.LANGWATCH_ENDPOINT ?? "https://app.langwatch.ai",
|
|
51
|
+
}: {
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
endpoint?: string;
|
|
54
|
+
} = {}) {
|
|
55
|
+
super();
|
|
56
|
+
const apiKey_ = apiKey ?? process.env.LANGWATCH_API_KEY;
|
|
57
|
+
if (!apiKey_) {
|
|
58
|
+
console.warn(
|
|
59
|
+
"[LangWatch] ⚠️ LangWatch API key is not set, please set the LANGWATCH_API_KEY environment variable or pass it in the constructor. Traces will not be captured."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
this.apiKey = apiKey_;
|
|
63
|
+
this.endpoint = endpoint;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getTrace({
|
|
67
|
+
traceId,
|
|
68
|
+
metadata,
|
|
69
|
+
}: { traceId?: string; metadata?: Metadata } = {}) {
|
|
70
|
+
return new LangWatchTrace({
|
|
71
|
+
client: this,
|
|
72
|
+
traceId: traceId ?? `trace_${nanoid()}`,
|
|
73
|
+
metadata,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async sendTrace(params: CollectorRESTParams) {
|
|
78
|
+
const backoff = [1000, 2000, 4000, 8000, 16000];
|
|
79
|
+
for (const backoffTime of backoff) {
|
|
80
|
+
try {
|
|
81
|
+
await this._sendTrace(params);
|
|
82
|
+
return;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`[LangWatch] ⚠️ Failed to send trace, retrying in ${
|
|
86
|
+
backoffTime / 1000
|
|
87
|
+
}s`
|
|
88
|
+
);
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, backoffTime));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.warn("[LangWatch] ⚠️ Failed to send trace, giving up");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _sendTrace(params: CollectorRESTParams) {
|
|
96
|
+
if (params.spans.length === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!this.apiKey) {
|
|
101
|
+
const error = new Error(
|
|
102
|
+
"[LangWatch] ⚠️ LangWatch API key is not set, LLMs traces will not be sent, go to https://langwatch.ai to set it up"
|
|
103
|
+
);
|
|
104
|
+
this.emit("error", error);
|
|
105
|
+
console.warn(error.message);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const response = await fetch(`${this.endpoint}/api/collector`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
"X-Auth-Token": this.apiKey,
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify(params),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (response.status === 429) {
|
|
119
|
+
const error = new Error(
|
|
120
|
+
"[LangWatch] ⚠️ Rate limit exceeded, dropping message from being sent to LangWatch. Please check your dashboard to upgrade your plan."
|
|
121
|
+
);
|
|
122
|
+
this.emit("error", error);
|
|
123
|
+
console.warn(error.message);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
const error = new Error(
|
|
128
|
+
`[LangWatch] ⚠️ Failed to send trace, status: ${response.status}`
|
|
129
|
+
);
|
|
130
|
+
this.emit("error", error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class LangWatchTrace {
|
|
137
|
+
client: LangWatch;
|
|
138
|
+
traceId: string;
|
|
139
|
+
metadata?: Metadata;
|
|
140
|
+
finishedSpans: Record<string, ServerSpan> = {};
|
|
141
|
+
timeoutRef?: NodeJS.Timeout;
|
|
142
|
+
|
|
143
|
+
constructor({
|
|
144
|
+
client,
|
|
145
|
+
traceId,
|
|
146
|
+
metadata,
|
|
147
|
+
}: {
|
|
148
|
+
client: LangWatch;
|
|
149
|
+
traceId: string;
|
|
150
|
+
metadata?: Metadata;
|
|
151
|
+
}) {
|
|
152
|
+
this.client = client;
|
|
153
|
+
this.traceId = traceId;
|
|
154
|
+
this.metadata = metadata;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
update({ metadata }: { metadata: Metadata }) {
|
|
158
|
+
this.metadata = {
|
|
159
|
+
...this.metadata,
|
|
160
|
+
...metadata,
|
|
161
|
+
...(typeof metadata.labels !== "undefined"
|
|
162
|
+
? { labels: [...(this.metadata?.labels ?? []), ...metadata.labels] }
|
|
163
|
+
: {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
startSpan(params: Omit<Partial<PendingBaseSpan>, "parentId">) {
|
|
168
|
+
const span = new LangWatchSpan({
|
|
169
|
+
trace: this,
|
|
170
|
+
...params,
|
|
171
|
+
});
|
|
172
|
+
return span;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
startLLMSpan(params: Omit<Partial<PendingLLMSpan>, "parentId">) {
|
|
176
|
+
const span = new LangWatchLLMSpan({
|
|
177
|
+
trace: this,
|
|
178
|
+
...params,
|
|
179
|
+
});
|
|
180
|
+
return span;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
startRAGSpan(params: Omit<Partial<PendingRAGSpan>, "parentId">) {
|
|
184
|
+
const span = new LangWatchRAGSpan({
|
|
185
|
+
trace: this,
|
|
186
|
+
...params,
|
|
187
|
+
});
|
|
188
|
+
return span;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onEnd(span: ServerSpan) {
|
|
192
|
+
this.finishedSpans[span.span_id] = span;
|
|
193
|
+
this.delayedSendSpans();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
delayedSendSpans() {
|
|
197
|
+
clearTimeout(this.timeoutRef);
|
|
198
|
+
this.timeoutRef = setTimeout(() => {
|
|
199
|
+
void this.sendSpans();
|
|
200
|
+
}, 1000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async sendSpans() {
|
|
204
|
+
clearTimeout(this.timeoutRef);
|
|
205
|
+
|
|
206
|
+
let trace: CollectorRESTParams | undefined = undefined;
|
|
207
|
+
try {
|
|
208
|
+
trace = collectorRESTParamsSchema.parse({
|
|
209
|
+
trace_id: this.traceId,
|
|
210
|
+
metadata: camelToSnakeCaseNested(this.metadata),
|
|
211
|
+
spans: Object.values(this.finishedSpans),
|
|
212
|
+
} as Strict<CollectorRESTParams>);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof ZodError) {
|
|
215
|
+
console.warn("[LangWatch] ⚠️ Failed to parse trace");
|
|
216
|
+
console.warn(fromZodError(error).message);
|
|
217
|
+
} else {
|
|
218
|
+
console.warn(error);
|
|
219
|
+
}
|
|
220
|
+
this.client.emit("error", error);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (trace) {
|
|
224
|
+
await this.client.sendTrace(trace);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class LangWatchSpan implements PendingBaseSpan {
|
|
230
|
+
trace: LangWatchTrace;
|
|
231
|
+
|
|
232
|
+
spanId: string;
|
|
233
|
+
parentId?: string | null;
|
|
234
|
+
type: SpanTypes;
|
|
235
|
+
name?: string | null;
|
|
236
|
+
input: PendingBaseSpan["input"];
|
|
237
|
+
outputs: PendingBaseSpan["outputs"];
|
|
238
|
+
error?: PendingBaseSpan["error"];
|
|
239
|
+
timestamps: PendingBaseSpan["timestamps"];
|
|
240
|
+
metrics: PendingBaseSpan["metrics"];
|
|
241
|
+
|
|
242
|
+
constructor({
|
|
243
|
+
trace,
|
|
244
|
+
spanId,
|
|
245
|
+
parentId,
|
|
246
|
+
type,
|
|
247
|
+
name,
|
|
248
|
+
input,
|
|
249
|
+
outputs,
|
|
250
|
+
error,
|
|
251
|
+
timestamps,
|
|
252
|
+
metrics,
|
|
253
|
+
}: Partial<PendingBaseSpan> & { trace: LangWatchTrace }) {
|
|
254
|
+
this.spanId = spanId ?? `span_${nanoid()}`;
|
|
255
|
+
this.parentId = parentId;
|
|
256
|
+
this.trace = trace;
|
|
257
|
+
this.type = type ?? "span";
|
|
258
|
+
this.name = name;
|
|
259
|
+
this.input = input;
|
|
260
|
+
this.outputs = outputs ?? [];
|
|
261
|
+
this.error = error;
|
|
262
|
+
this.timestamps = timestamps ?? {
|
|
263
|
+
startedAt: Date.now(),
|
|
264
|
+
};
|
|
265
|
+
this.metrics = metrics;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
update(params: Partial<Omit<PendingBaseSpan, "spanId" | "parentId">>) {
|
|
269
|
+
if (params.type) {
|
|
270
|
+
this.type = params.type;
|
|
271
|
+
}
|
|
272
|
+
if ("name" in params) {
|
|
273
|
+
this.name = params.name;
|
|
274
|
+
}
|
|
275
|
+
if ("input" in params) {
|
|
276
|
+
this.input = params.input;
|
|
277
|
+
}
|
|
278
|
+
if (params.outputs) {
|
|
279
|
+
this.outputs = params.outputs;
|
|
280
|
+
}
|
|
281
|
+
if ("error" in params) {
|
|
282
|
+
this.error = params.error;
|
|
283
|
+
}
|
|
284
|
+
if (params.timestamps) {
|
|
285
|
+
this.timestamps = params.timestamps;
|
|
286
|
+
}
|
|
287
|
+
if ("metrics" in params) {
|
|
288
|
+
this.metrics = params.metrics;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
startSpan(params: Omit<Partial<PendingBaseSpan>, "parentId">) {
|
|
293
|
+
const span = new LangWatchSpan({
|
|
294
|
+
trace: this.trace,
|
|
295
|
+
parentId: this.spanId,
|
|
296
|
+
...params,
|
|
297
|
+
});
|
|
298
|
+
return span;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
startLLMSpan(params: Omit<Partial<PendingLLMSpan>, "parentId">) {
|
|
302
|
+
const span = new LangWatchLLMSpan({
|
|
303
|
+
trace: this.trace,
|
|
304
|
+
parentId: this.spanId,
|
|
305
|
+
...params,
|
|
306
|
+
});
|
|
307
|
+
return span;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
startRAGSpan(params: Omit<Partial<PendingRAGSpan>, "parentId">) {
|
|
311
|
+
const span = new LangWatchRAGSpan({
|
|
312
|
+
trace: this.trace,
|
|
313
|
+
parentId: this.spanId,
|
|
314
|
+
...params,
|
|
315
|
+
});
|
|
316
|
+
return span;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
end(params?: Partial<Omit<PendingBaseSpan, "spanId" | "parentId">>) {
|
|
320
|
+
this.timestamps.finishedAt = Date.now();
|
|
321
|
+
if (params) {
|
|
322
|
+
this.update(params);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const finalSpan = spanSchema.parse(
|
|
327
|
+
camelToSnakeCaseNested({
|
|
328
|
+
...this,
|
|
329
|
+
trace: undefined,
|
|
330
|
+
traceId: this.trace.traceId,
|
|
331
|
+
timestamps: {
|
|
332
|
+
...this.timestamps,
|
|
333
|
+
finishedAt: this.timestamps.finishedAt,
|
|
334
|
+
},
|
|
335
|
+
}) as ServerSpan
|
|
336
|
+
);
|
|
337
|
+
this.trace.onEnd(finalSpan);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error instanceof ZodError) {
|
|
340
|
+
console.warn("[LangWatch] ⚠️ Failed to parse span");
|
|
341
|
+
console.warn(fromZodError(error).message);
|
|
342
|
+
} else {
|
|
343
|
+
console.warn(error);
|
|
344
|
+
}
|
|
345
|
+
this.trace.client.emit("error", error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export class LangWatchLLMSpan extends LangWatchSpan implements PendingLLMSpan {
|
|
351
|
+
type: "llm";
|
|
352
|
+
model: PendingLLMSpan["model"];
|
|
353
|
+
params: PendingLLMSpan["params"];
|
|
354
|
+
|
|
355
|
+
constructor(params: Partial<PendingLLMSpan> & { trace: LangWatchTrace }) {
|
|
356
|
+
super({ ...params });
|
|
357
|
+
this.type = "llm";
|
|
358
|
+
this.model = params.model ?? "unknown";
|
|
359
|
+
this.params = params.params ?? {};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
update(params: Partial<PendingLLMSpan>) {
|
|
363
|
+
super.update(params);
|
|
364
|
+
if (params.model) {
|
|
365
|
+
this.model = params.model;
|
|
366
|
+
}
|
|
367
|
+
if (params.params) {
|
|
368
|
+
this.params = params.params;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
end(params?: Partial<PendingLLMSpan>) {
|
|
373
|
+
super.end(params);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export class LangWatchRAGSpan extends LangWatchSpan implements PendingRAGSpan {
|
|
378
|
+
type: "rag";
|
|
379
|
+
contexts: PendingRAGSpan["contexts"];
|
|
380
|
+
|
|
381
|
+
constructor(params: Partial<PendingRAGSpan> & { trace: LangWatchTrace }) {
|
|
382
|
+
super({ ...params });
|
|
383
|
+
this.type = "rag";
|
|
384
|
+
this.contexts = params.contexts ?? [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
update(params: Partial<PendingRAGSpan>) {
|
|
388
|
+
super.update(params);
|
|
389
|
+
if (params.contexts) {
|
|
390
|
+
this.contexts = params.contexts;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
end(params?: Partial<PendingRAGSpan>) {
|
|
395
|
+
super.end(params);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
File without changes
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type modelPrices from "llm-cost/model_prices_and_context_window.json";
|
|
2
|
+
import type { OpenAI } from "openai";
|
|
3
|
+
import { type SnakeToCamelCaseNested } from "./helpers";
|
|
4
|
+
import {
|
|
5
|
+
type BaseSpan as ServerBaseSpan,
|
|
6
|
+
type ChatMessage as ServerChatMessage,
|
|
7
|
+
type ChatRichContent as ServerChatRichContent,
|
|
8
|
+
type LLMSpan as ServerLLMSpan,
|
|
9
|
+
type RAGSpan as ServerRAGSpan,
|
|
10
|
+
type SpanInputOutput as ServerSpanInputOutput,
|
|
11
|
+
type TypedValueChatMessages,
|
|
12
|
+
type Trace,
|
|
13
|
+
} from "./server/types/tracer";
|
|
14
|
+
|
|
15
|
+
export type Metadata = SnakeToCamelCaseNested<Trace["metadata"]>;
|
|
16
|
+
|
|
17
|
+
export type ChatMessage = ServerChatMessage;
|
|
18
|
+
|
|
19
|
+
export type ChatRichContent = ServerChatRichContent;
|
|
20
|
+
|
|
21
|
+
// Check to see if out ChatMessage type is compatible with OpenAIChatCompletion messages
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
+
({}) as OpenAI.Chat.ChatCompletionMessageParam satisfies ChatMessage;
|
|
24
|
+
// Check to see spans input/output is still compatible with OpenAIChatCompletion messages to avoid camelCase/snake_case issues
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
26
|
+
({}) as {
|
|
27
|
+
type: "chat_messages";
|
|
28
|
+
value: OpenAI.Chat.ChatCompletionMessageParam[];
|
|
29
|
+
} satisfies BaseSpan["input"];
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
|
+
({}) as {
|
|
32
|
+
type: "chat_messages";
|
|
33
|
+
value: OpenAI.Chat.ChatCompletionMessageParam[];
|
|
34
|
+
}[] satisfies BaseSpan["outputs"];
|
|
35
|
+
|
|
36
|
+
// Keep the input/output types signatures as snake case to match the official openai nodejs api
|
|
37
|
+
export type SpanInputOutput =
|
|
38
|
+
| SnakeToCamelCaseNested<
|
|
39
|
+
Exclude<ServerSpanInputOutput, TypedValueChatMessages>
|
|
40
|
+
>
|
|
41
|
+
| (TypedValueChatMessages & { type: ChatMessage });
|
|
42
|
+
|
|
43
|
+
export type ConvertServerSpan<T extends ServerBaseSpan> =
|
|
44
|
+
SnakeToCamelCaseNested<Omit<T, "input" | "outputs">> & {
|
|
45
|
+
input?: SpanInputOutput | null;
|
|
46
|
+
outputs: SpanInputOutput[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type PendingSpan<T extends BaseSpan> = Omit<
|
|
50
|
+
T,
|
|
51
|
+
"traceId" | "timestamps"
|
|
52
|
+
> & {
|
|
53
|
+
timestamps: Omit<T["timestamps"], "finishedAt"> & {
|
|
54
|
+
finishedAt?: number | null;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type BaseSpan = ConvertServerSpan<ServerBaseSpan>;
|
|
59
|
+
|
|
60
|
+
export type PendingBaseSpan = PendingSpan<BaseSpan>;
|
|
61
|
+
|
|
62
|
+
// vendor is deprecated, and we try to force the available models here
|
|
63
|
+
export type LLMSpan = ConvertServerSpan<
|
|
64
|
+
Omit<ServerLLMSpan, "vendor" | "model">
|
|
65
|
+
> & { model: keyof typeof modelPrices | (string & NonNullable<unknown>) };
|
|
66
|
+
export type PendingLLMSpan = PendingSpan<LLMSpan>;
|
|
67
|
+
|
|
68
|
+
export type RAGSpan = ConvertServerSpan<ServerRAGSpan>;
|
|
69
|
+
export type PendingRAGSpan = PendingSpan<RAGSpan>;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { convertUint8ArrayToBase64 } from "@ai-sdk/provider-utils";
|
|
2
|
+
import { type ImagePart, type CoreMessage } from "ai";
|
|
3
|
+
import { type ChatMessage } from "./types";
|
|
4
|
+
|
|
5
|
+
const convertImageToUrl = (
|
|
6
|
+
image: ImagePart["image"],
|
|
7
|
+
mimeType: string | undefined
|
|
8
|
+
) => {
|
|
9
|
+
try {
|
|
10
|
+
return image instanceof URL
|
|
11
|
+
? image.toString()
|
|
12
|
+
: typeof image === "string"
|
|
13
|
+
? image
|
|
14
|
+
: `data:${mimeType ?? "image/jpeg"};base64,${convertUint8ArrayToBase64(
|
|
15
|
+
image as any
|
|
16
|
+
)}`;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error("[LangWatch] error converting vercel ui image to url:", e);
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Mostly copied from https://github.com/vercel/ai/blob/main/packages/openai/src/convert-to-openai-chat-messages.ts
|
|
24
|
+
export function convertFromVercelAIMessages(
|
|
25
|
+
messages: CoreMessage[]
|
|
26
|
+
): ChatMessage[] {
|
|
27
|
+
const lwMessages: ChatMessage[] = [];
|
|
28
|
+
|
|
29
|
+
for (const { role, content } of messages) {
|
|
30
|
+
switch (role) {
|
|
31
|
+
case "system": {
|
|
32
|
+
lwMessages.push({ role: "system", content });
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case "user": {
|
|
37
|
+
if (
|
|
38
|
+
Array.isArray(content) &&
|
|
39
|
+
content.length === 1 &&
|
|
40
|
+
content[0]?.type === "text"
|
|
41
|
+
) {
|
|
42
|
+
lwMessages.push({ role: "user", content: content[0].text });
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
lwMessages.push({
|
|
47
|
+
role: "user",
|
|
48
|
+
content: Array.isArray(content)
|
|
49
|
+
? content.map((part) => {
|
|
50
|
+
switch (part.type) {
|
|
51
|
+
case "text": {
|
|
52
|
+
return { type: "text", text: part.text };
|
|
53
|
+
}
|
|
54
|
+
case "image": {
|
|
55
|
+
return {
|
|
56
|
+
type: "image_url",
|
|
57
|
+
image_url: {
|
|
58
|
+
url: convertImageToUrl(part.image, part.mimeType),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
: content,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "assistant": {
|
|
71
|
+
let text = "";
|
|
72
|
+
const toolCalls: Array<{
|
|
73
|
+
id: string;
|
|
74
|
+
type: "function";
|
|
75
|
+
function: { name: string; arguments: string };
|
|
76
|
+
}> = [];
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(content)) {
|
|
79
|
+
for (const part of content) {
|
|
80
|
+
switch (part.type) {
|
|
81
|
+
case "text": {
|
|
82
|
+
text += part.text;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "tool-call": {
|
|
86
|
+
toolCalls.push({
|
|
87
|
+
id: part.toolCallId,
|
|
88
|
+
type: "function",
|
|
89
|
+
function: {
|
|
90
|
+
name: part.toolName,
|
|
91
|
+
arguments: JSON.stringify(part.args),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
default: {
|
|
97
|
+
const _exhaustiveCheck = part;
|
|
98
|
+
throw new Error(`Unsupported part: ${_exhaustiveCheck as any}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
text = content;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
lwMessages.push({
|
|
107
|
+
role: "assistant",
|
|
108
|
+
content: text,
|
|
109
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case "tool": {
|
|
116
|
+
for (const toolResponse of content) {
|
|
117
|
+
lwMessages.push({
|
|
118
|
+
role: "tool",
|
|
119
|
+
tool_call_id: toolResponse.toolCallId,
|
|
120
|
+
content: JSON.stringify(toolResponse.result),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
default: {
|
|
127
|
+
const _exhaustiveCheck = role;
|
|
128
|
+
throw new Error(`Unsupported role: ${_exhaustiveCheck as any}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lwMessages;
|
|
134
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-to-zod configuration.
|
|
3
|
+
*
|
|
4
|
+
* @type {import("ts-to-zod").TsToZodConfig}
|
|
5
|
+
*/
|
|
6
|
+
module.exports = {
|
|
7
|
+
input: "server/tracer/types.ts",
|
|
8
|
+
output: "server/tracer/types.zod.ts",
|
|
9
|
+
nameFilter: (name) =>
|
|
10
|
+
![
|
|
11
|
+
"ElasticSearchSpan",
|
|
12
|
+
"ElasticSearchInputOutput",
|
|
13
|
+
"EvaluatorDefinition",
|
|
14
|
+
"TraceCheckJob",
|
|
15
|
+
"AnalyticsMetric",
|
|
16
|
+
"NewDatasetEntries",
|
|
17
|
+
].includes(name),
|
|
18
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2017",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"checkJs": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"jsx": "preserve",
|
|
18
|
+
"incremental": true,
|
|
19
|
+
"noUncheckedIndexedAccess": true,
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
".eslintrc.cjs",
|
|
25
|
+
"next-env.d.ts",
|
|
26
|
+
"**/*.ts",
|
|
27
|
+
"**/*.tsx",
|
|
28
|
+
"**/*.cjs",
|
|
29
|
+
"**/*.mjs"
|
|
30
|
+
],
|
|
31
|
+
"exclude": ["node_modules", "./dist/**/*", "./example/**/*"]
|
|
32
|
+
}
|
package/tsup.config.ts
ADDED