langwatch 0.0.2 → 0.1.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/dist/{chunk-GOA2HL4A.mjs → chunk-AP23NJ57.mjs} +29 -2
- package/dist/{chunk-GOA2HL4A.mjs.map → chunk-AP23NJ57.mjs.map} +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +66 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41 -30
- package/dist/index.mjs.map +1 -1
- package/dist/{utils-s3gGR6vj.d.mts → utils-DDcm0z9v.d.mts} +10 -5
- package/dist/{utils-s3gGR6vj.d.ts → utils-DDcm0z9v.d.ts} +10 -5
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +28 -0
- package/dist/utils.js.map +1 -1
- package/dist/utils.mjs +3 -1
- package/example/lib/chat/actions.tsx +4 -6
- package/example/package-lock.json +3 -2
- package/package.json +3 -2
- package/src/index.test.ts +137 -28
- package/src/index.ts +25 -19
- package/src/types.ts +4 -3
- package/src/utils.ts +45 -0
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpenAI } from "openai";
|
|
1
|
+
import { AzureOpenAI, OpenAI } from "openai";
|
|
2
2
|
import { LangWatch, convertFromVercelAIMessages } from "./index";
|
|
3
3
|
import {
|
|
4
4
|
describe,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "vitest";
|
|
11
11
|
import { openai } from "@ai-sdk/openai";
|
|
12
12
|
import { generateText, type CoreMessage } from "ai";
|
|
13
|
+
import "dotenv/config";
|
|
13
14
|
|
|
14
15
|
describe("LangWatch tracer", () => {
|
|
15
16
|
let mockFetch: SpyInstanceFn;
|
|
@@ -52,14 +53,12 @@ describe("LangWatch tracer", () => {
|
|
|
52
53
|
},
|
|
53
54
|
});
|
|
54
55
|
span.end({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
weather: "sunny",
|
|
60
|
-
},
|
|
56
|
+
output: {
|
|
57
|
+
type: "json",
|
|
58
|
+
value: {
|
|
59
|
+
weather: "sunny",
|
|
61
60
|
},
|
|
62
|
-
|
|
61
|
+
},
|
|
63
62
|
});
|
|
64
63
|
|
|
65
64
|
expect(trace.metadata).toEqual({
|
|
@@ -99,17 +98,15 @@ describe("LangWatch tracer", () => {
|
|
|
99
98
|
},
|
|
100
99
|
});
|
|
101
100
|
llmSpan.end({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
},
|
|
112
|
-
],
|
|
101
|
+
output: {
|
|
102
|
+
type: "chat_messages",
|
|
103
|
+
value: [
|
|
104
|
+
{
|
|
105
|
+
role: "assistant",
|
|
106
|
+
content: "It's cloudy in Tokyo.",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
113
110
|
});
|
|
114
111
|
|
|
115
112
|
ragSpan.end();
|
|
@@ -140,6 +137,47 @@ describe("LangWatch tracer", () => {
|
|
|
140
137
|
expect(requestBody.spans.length).toBe(3);
|
|
141
138
|
});
|
|
142
139
|
|
|
140
|
+
it("captures exceptions", async () => {
|
|
141
|
+
const langwatch = new LangWatch({
|
|
142
|
+
apiKey: "test",
|
|
143
|
+
endpoint: "http://localhost.test",
|
|
144
|
+
});
|
|
145
|
+
const trace = langwatch.getTrace();
|
|
146
|
+
trace.update({
|
|
147
|
+
metadata: { threadId: "123", userId: "123", labels: ["foo"] },
|
|
148
|
+
});
|
|
149
|
+
trace.update({ metadata: { userId: "456", labels: ["bar"] } });
|
|
150
|
+
|
|
151
|
+
const span = trace.startSpan({
|
|
152
|
+
name: "weather_function",
|
|
153
|
+
input: {
|
|
154
|
+
type: "json",
|
|
155
|
+
value: {
|
|
156
|
+
city: "Tokyo",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
throw new Error("unexpected error");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
span.end({
|
|
165
|
+
error: error,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await trace.sendSpans();
|
|
170
|
+
|
|
171
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
172
|
+
const firstCall: any = mockFetch.mock.calls[0];
|
|
173
|
+
const requestBody = JSON.parse(firstCall[1].body);
|
|
174
|
+
expect(requestBody.spans[0].error).toEqual({
|
|
175
|
+
has_error: true,
|
|
176
|
+
message: "unexpected error",
|
|
177
|
+
stacktrace: expect.any(Array),
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
143
181
|
it.skip("captures openai llm call", async () => {
|
|
144
182
|
const langwatch = new LangWatch({
|
|
145
183
|
apiKey: "test",
|
|
@@ -175,10 +213,79 @@ describe("LangWatch tracer", () => {
|
|
|
175
213
|
});
|
|
176
214
|
|
|
177
215
|
span.end({
|
|
178
|
-
|
|
216
|
+
output: {
|
|
179
217
|
type: "chat_messages",
|
|
180
|
-
value: [
|
|
181
|
-
}
|
|
218
|
+
value: [chatCompletion.choices[0]!.message],
|
|
219
|
+
},
|
|
220
|
+
metrics: {
|
|
221
|
+
promptTokens: chatCompletion.usage?.prompt_tokens,
|
|
222
|
+
completionTokens: chatCompletion.usage?.completion_tokens,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await trace.sendSpans();
|
|
227
|
+
|
|
228
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
229
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
230
|
+
"http://localhost.test/api/collector",
|
|
231
|
+
expect.objectContaining({
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: {
|
|
234
|
+
"X-Auth-Token": "test",
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
},
|
|
237
|
+
body: expect.any(String),
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it.skip("captures azure openai llm call", async () => {
|
|
243
|
+
const langwatch = new LangWatch({
|
|
244
|
+
apiKey: "test",
|
|
245
|
+
endpoint: "http://localhost.test",
|
|
246
|
+
});
|
|
247
|
+
const trace = langwatch.getTrace();
|
|
248
|
+
|
|
249
|
+
// Model to be used and messages that will be sent to the LLM
|
|
250
|
+
const model = "gpt-4-turbo-2024-04-09";
|
|
251
|
+
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
252
|
+
{ role: "system", content: "You are a helpful assistant." },
|
|
253
|
+
{
|
|
254
|
+
role: "user",
|
|
255
|
+
content: "Write a tweet-size vegetarian lasagna recipe for 4 people.",
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
// Capture the llm call with a span
|
|
260
|
+
const span = trace.startLLMSpan({
|
|
261
|
+
name: "llm",
|
|
262
|
+
model: model,
|
|
263
|
+
input: {
|
|
264
|
+
type: "chat_messages",
|
|
265
|
+
value: messages,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Continue with the LLM call normally
|
|
270
|
+
const openai = new AzureOpenAI({
|
|
271
|
+
apiKey: process.env.AZURE_OPENAI_API_KEY,
|
|
272
|
+
apiVersion: "2024-02-01",
|
|
273
|
+
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
|
|
274
|
+
});
|
|
275
|
+
const chatCompletion = await openai.chat.completions.create({
|
|
276
|
+
messages: messages,
|
|
277
|
+
model: model,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
span.end({
|
|
281
|
+
output: {
|
|
282
|
+
type: "chat_messages",
|
|
283
|
+
value: [chatCompletion.choices[0]!.message],
|
|
284
|
+
},
|
|
285
|
+
metrics: {
|
|
286
|
+
promptTokens: chatCompletion.usage?.prompt_tokens,
|
|
287
|
+
completionTokens: chatCompletion.usage?.completion_tokens,
|
|
288
|
+
},
|
|
182
289
|
});
|
|
183
290
|
|
|
184
291
|
await trace.sendSpans();
|
|
@@ -229,12 +336,14 @@ describe("LangWatch tracer", () => {
|
|
|
229
336
|
});
|
|
230
337
|
|
|
231
338
|
span.end({
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
339
|
+
output: {
|
|
340
|
+
type: "chat_messages",
|
|
341
|
+
value: convertFromVercelAIMessages(response.responseMessages),
|
|
342
|
+
},
|
|
343
|
+
metrics: {
|
|
344
|
+
promptTokens: response.usage?.promptTokens,
|
|
345
|
+
completionTokens: response.usage?.completionTokens,
|
|
346
|
+
},
|
|
238
347
|
});
|
|
239
348
|
|
|
240
349
|
await trace.sendSpans();
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type RAGSpan,
|
|
25
25
|
type SpanInputOutput,
|
|
26
26
|
} from "./types";
|
|
27
|
-
import { convertFromVercelAIMessages } from "./utils";
|
|
27
|
+
import { captureError, convertFromVercelAIMessages } from "./utils";
|
|
28
28
|
|
|
29
29
|
export type {
|
|
30
30
|
BaseSpan,
|
|
@@ -39,7 +39,7 @@ export type {
|
|
|
39
39
|
SpanInputOutput,
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
export { convertFromVercelAIMessages };
|
|
42
|
+
export { convertFromVercelAIMessages, captureError };
|
|
43
43
|
|
|
44
44
|
export class LangWatch extends EventEmitter {
|
|
45
45
|
apiKey: string | undefined;
|
|
@@ -55,9 +55,10 @@ export class LangWatch extends EventEmitter {
|
|
|
55
55
|
super();
|
|
56
56
|
const apiKey_ = apiKey ?? process.env.LANGWATCH_API_KEY;
|
|
57
57
|
if (!apiKey_) {
|
|
58
|
-
|
|
59
|
-
"
|
|
58
|
+
const error = new Error(
|
|
59
|
+
"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
60
|
);
|
|
61
|
+
this.emit("error", error);
|
|
61
62
|
}
|
|
62
63
|
this.apiKey = apiKey_;
|
|
63
64
|
this.endpoint = endpoint;
|
|
@@ -99,10 +100,9 @@ export class LangWatch extends EventEmitter {
|
|
|
99
100
|
|
|
100
101
|
if (!this.apiKey) {
|
|
101
102
|
const error = new Error(
|
|
102
|
-
"
|
|
103
|
+
"LangWatch API key is not set, LLMs traces will not be sent, go to https://langwatch.ai to set it up"
|
|
103
104
|
);
|
|
104
105
|
this.emit("error", error);
|
|
105
|
-
console.warn(error.message);
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -117,15 +117,14 @@ export class LangWatch extends EventEmitter {
|
|
|
117
117
|
|
|
118
118
|
if (response.status === 429) {
|
|
119
119
|
const error = new Error(
|
|
120
|
-
"
|
|
120
|
+
"Rate limit exceeded, dropping message from being sent to LangWatch. Please check your dashboard to upgrade your plan."
|
|
121
121
|
);
|
|
122
122
|
this.emit("error", error);
|
|
123
|
-
console.warn(error.message);
|
|
124
123
|
return;
|
|
125
124
|
}
|
|
126
125
|
if (!response.ok) {
|
|
127
126
|
const error = new Error(
|
|
128
|
-
`
|
|
127
|
+
`Failed to send trace, status: ${response.status}`
|
|
129
128
|
);
|
|
130
129
|
this.emit("error", error);
|
|
131
130
|
throw error;
|
|
@@ -214,8 +213,6 @@ export class LangWatchTrace {
|
|
|
214
213
|
if (error instanceof ZodError) {
|
|
215
214
|
console.warn("[LangWatch] ⚠️ Failed to parse trace");
|
|
216
215
|
console.warn(fromZodError(error).message);
|
|
217
|
-
} else {
|
|
218
|
-
console.warn(error);
|
|
219
216
|
}
|
|
220
217
|
this.client.emit("error", error);
|
|
221
218
|
}
|
|
@@ -233,8 +230,8 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
233
230
|
parentId?: string | null;
|
|
234
231
|
type: SpanTypes;
|
|
235
232
|
name?: string | null;
|
|
236
|
-
input
|
|
237
|
-
|
|
233
|
+
input?: PendingBaseSpan["input"];
|
|
234
|
+
output?: PendingBaseSpan["output"];
|
|
238
235
|
error?: PendingBaseSpan["error"];
|
|
239
236
|
timestamps: PendingBaseSpan["timestamps"];
|
|
240
237
|
metrics: PendingBaseSpan["metrics"];
|
|
@@ -246,7 +243,7 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
246
243
|
type,
|
|
247
244
|
name,
|
|
248
245
|
input,
|
|
249
|
-
|
|
246
|
+
output,
|
|
250
247
|
error,
|
|
251
248
|
timestamps,
|
|
252
249
|
metrics,
|
|
@@ -257,7 +254,7 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
257
254
|
this.type = type ?? "span";
|
|
258
255
|
this.name = name;
|
|
259
256
|
this.input = input;
|
|
260
|
-
this.
|
|
257
|
+
this.output = output;
|
|
261
258
|
this.error = error;
|
|
262
259
|
this.timestamps = timestamps ?? {
|
|
263
260
|
startedAt: Date.now(),
|
|
@@ -266,6 +263,14 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
266
263
|
}
|
|
267
264
|
|
|
268
265
|
update(params: Partial<Omit<PendingBaseSpan, "spanId" | "parentId">>) {
|
|
266
|
+
if (Object.isFrozen(this)) {
|
|
267
|
+
const error = new Error(
|
|
268
|
+
`Tried to update span ${this.spanId}, but the span is already finished, discarding update`
|
|
269
|
+
);
|
|
270
|
+
this.trace.client.emit("error", error);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
269
274
|
if (params.type) {
|
|
270
275
|
this.type = params.type;
|
|
271
276
|
}
|
|
@@ -275,8 +280,8 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
275
280
|
if ("input" in params) {
|
|
276
281
|
this.input = params.input;
|
|
277
282
|
}
|
|
278
|
-
if (params
|
|
279
|
-
this.
|
|
283
|
+
if ("output" in params) {
|
|
284
|
+
this.output = params.output;
|
|
280
285
|
}
|
|
281
286
|
if ("error" in params) {
|
|
282
287
|
this.error = params.error;
|
|
@@ -322,6 +327,8 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
322
327
|
this.update(params);
|
|
323
328
|
}
|
|
324
329
|
|
|
330
|
+
Object.freeze(this);
|
|
331
|
+
|
|
325
332
|
try {
|
|
326
333
|
const finalSpan = spanSchema.parse(
|
|
327
334
|
camelToSnakeCaseNested({
|
|
@@ -332,6 +339,7 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
332
339
|
...this.timestamps,
|
|
333
340
|
finishedAt: this.timestamps.finishedAt,
|
|
334
341
|
},
|
|
342
|
+
...(this.error && { error: captureError(this.error) }),
|
|
335
343
|
}) as ServerSpan
|
|
336
344
|
);
|
|
337
345
|
this.trace.onEnd(finalSpan);
|
|
@@ -339,8 +347,6 @@ export class LangWatchSpan implements PendingBaseSpan {
|
|
|
339
347
|
if (error instanceof ZodError) {
|
|
340
348
|
console.warn("[LangWatch] ⚠️ Failed to parse span");
|
|
341
349
|
console.warn(fromZodError(error).message);
|
|
342
|
-
} else {
|
|
343
|
-
console.warn(error);
|
|
344
350
|
}
|
|
345
351
|
this.trace.client.emit("error", error);
|
|
346
352
|
}
|
package/src/types.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type ChatRichContent = ServerChatRichContent;
|
|
|
31
31
|
({}) as {
|
|
32
32
|
type: "chat_messages";
|
|
33
33
|
value: OpenAI.Chat.ChatCompletionMessageParam[];
|
|
34
|
-
}
|
|
34
|
+
} satisfies BaseSpan["output"];
|
|
35
35
|
|
|
36
36
|
// Keep the input/output types signatures as snake case to match the official openai nodejs api
|
|
37
37
|
export type SpanInputOutput =
|
|
@@ -41,9 +41,10 @@ export type SpanInputOutput =
|
|
|
41
41
|
| (TypedValueChatMessages & { type: ChatMessage });
|
|
42
42
|
|
|
43
43
|
export type ConvertServerSpan<T extends ServerBaseSpan> =
|
|
44
|
-
SnakeToCamelCaseNested<Omit<T, "input" | "
|
|
44
|
+
SnakeToCamelCaseNested<Omit<T, "input" | "output" | "error">> & {
|
|
45
45
|
input?: SpanInputOutput | null;
|
|
46
|
-
|
|
46
|
+
output?: SpanInputOutput | null;
|
|
47
|
+
error?: T["error"] | NonNullable<unknown>;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
export type PendingSpan<T extends BaseSpan> = Omit<
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { convertUint8ArrayToBase64 } from "@ai-sdk/provider-utils";
|
|
2
2
|
import { type ImagePart, type CoreMessage } from "ai";
|
|
3
3
|
import { type ChatMessage } from "./types";
|
|
4
|
+
import { type ErrorCapture } from "./server/types/tracer";
|
|
4
5
|
|
|
5
6
|
const convertImageToUrl = (
|
|
6
7
|
image: ImagePart["image"],
|
|
@@ -132,3 +133,47 @@ export function convertFromVercelAIMessages(
|
|
|
132
133
|
|
|
133
134
|
return lwMessages;
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
export const captureError = (error: unknown): ErrorCapture => {
|
|
138
|
+
if (
|
|
139
|
+
error &&
|
|
140
|
+
typeof error === "object" &&
|
|
141
|
+
"has_error" in error &&
|
|
142
|
+
"message" in error &&
|
|
143
|
+
"stacktrace" in error
|
|
144
|
+
) {
|
|
145
|
+
return error as ErrorCapture;
|
|
146
|
+
} else if (error instanceof Error) {
|
|
147
|
+
return {
|
|
148
|
+
has_error: true,
|
|
149
|
+
message: error.message,
|
|
150
|
+
stacktrace: error.stack ? error.stack.split("\n") : [],
|
|
151
|
+
};
|
|
152
|
+
} else if (typeof error === "object" && error !== null) {
|
|
153
|
+
const err = error as { message: unknown; stack: unknown };
|
|
154
|
+
const message =
|
|
155
|
+
typeof err.message === "string"
|
|
156
|
+
? err.message
|
|
157
|
+
: "An unknown error occurred";
|
|
158
|
+
const stacktrace =
|
|
159
|
+
typeof err.stack === "string"
|
|
160
|
+
? err.stack.split("\n")
|
|
161
|
+
: Array.isArray(err.stack) &&
|
|
162
|
+
err.stack.length > 0 &&
|
|
163
|
+
typeof err.stack[0] === "string"
|
|
164
|
+
? err.stack
|
|
165
|
+
: ["No stack trace available"];
|
|
166
|
+
return {
|
|
167
|
+
has_error: true,
|
|
168
|
+
message,
|
|
169
|
+
stacktrace,
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
// Handle primitives and other types that are not an error object
|
|
173
|
+
return {
|
|
174
|
+
has_error: true,
|
|
175
|
+
message: String(error),
|
|
176
|
+
stacktrace: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|