langwatch 0.1.0 → 0.1.3

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.
Files changed (46) hide show
  1. package/copy-types.sh +17 -0
  2. package/dist/chunk-2I4YLOQY.mjs +736 -0
  3. package/dist/chunk-2I4YLOQY.mjs.map +1 -0
  4. package/dist/index.d.mts +352 -4
  5. package/dist/index.d.ts +352 -4
  6. package/dist/index.js +6505 -413
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +571 -359
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/{utils-DDcm0z9v.d.mts → utils-CFtM8VVg.d.mts} +108 -31
  11. package/dist/{utils-DDcm0z9v.d.ts → utils-CFtM8VVg.d.ts} +108 -31
  12. package/dist/utils.d.mts +1 -2
  13. package/dist/utils.d.ts +1 -2
  14. package/dist/utils.js +437 -0
  15. package/dist/utils.js.map +1 -1
  16. package/dist/utils.mjs +3 -1
  17. package/example/README.md +3 -1
  18. package/example/app/(chat)/chat/[id]/page.tsx +1 -1
  19. package/example/app/(chat)/page.tsx +10 -5
  20. package/example/app/guardrails/page.tsx +26 -0
  21. package/example/app/langchain/page.tsx +27 -0
  22. package/example/app/langchain-rag/page.tsx +28 -0
  23. package/example/app/share/[id]/page.tsx +1 -1
  24. package/example/components/chat-list.tsx +1 -1
  25. package/example/components/chat-panel.tsx +1 -1
  26. package/example/components/header.tsx +39 -13
  27. package/example/components/prompt-form.tsx +1 -1
  28. package/example/components/stocks/stock-purchase.tsx +1 -1
  29. package/example/components/stocks/stocks.tsx +1 -1
  30. package/example/lib/chat/guardrails.tsx +181 -0
  31. package/example/lib/chat/langchain-rag.tsx +191 -0
  32. package/example/lib/chat/langchain.tsx +112 -0
  33. package/example/lib/chat/{actions.tsx → vercel-ai.tsx} +1 -1
  34. package/example/package-lock.json +289 -6
  35. package/example/package.json +1 -0
  36. package/package.json +13 -5
  37. package/src/evaluations.ts +219 -0
  38. package/src/index.test.ts +5 -0
  39. package/src/index.ts +190 -7
  40. package/src/langchain.ts +557 -0
  41. package/src/{helpers.ts → typeUtils.ts} +20 -2
  42. package/src/types.ts +7 -3
  43. package/src/utils.ts +25 -2
  44. package/ts-to-zod.config.js +2 -0
  45. package/dist/chunk-AP23NJ57.mjs +0 -296
  46. package/dist/chunk-AP23NJ57.mjs.map +0 -1
@@ -0,0 +1,219 @@
1
+ import { type LangWatchSpan, type LangWatchTrace } from "./index";
2
+ import { type Conversation } from "./server/types/evaluations";
3
+ import {
4
+ type Evaluators,
5
+ type EvaluatorTypes,
6
+ } from "./server/types/evaluators.generated";
7
+ import {
8
+ type RAGChunk,
9
+ type SpanTypes,
10
+ type TypedValueEvaluationResult,
11
+ type TypedValueGuardrailResult,
12
+ type TypedValueJson,
13
+ } from "./server/types/tracer";
14
+
15
+ type Money = {
16
+ currency: string;
17
+ amount: number;
18
+ };
19
+
20
+ export type EvaluationResultModel = {
21
+ status: "processed" | "skipped" | "error";
22
+ passed?: boolean;
23
+ score?: number;
24
+ details?: string;
25
+ label?: string;
26
+ cost?: Money;
27
+ };
28
+
29
+ export type CommonEvaluationParams = {
30
+ name?: string;
31
+ input?: string;
32
+ output?: string;
33
+ expectedOutput?: string;
34
+ contexts?: RAGChunk[] | string[];
35
+ conversation?: Conversation;
36
+ asGuardrail?: boolean;
37
+ trace?: LangWatchTrace;
38
+ span?: LangWatchSpan;
39
+ };
40
+
41
+ export type SavedEvaluationParams = {
42
+ slug: string;
43
+ settings?: Record<string, unknown>;
44
+ } & CommonEvaluationParams;
45
+
46
+ export type LangEvalsEvaluationParams<T extends EvaluatorTypes> = {
47
+ evaluator: T;
48
+ settings?: Evaluators[T]["settings"];
49
+ } & CommonEvaluationParams;
50
+
51
+ export type EvaluationParams =
52
+ | SavedEvaluationParams
53
+ | LangEvalsEvaluationParams<EvaluatorTypes>;
54
+
55
+ export const evaluate = async (
56
+ params: EvaluationParams
57
+ ): Promise<EvaluationResultModel> => {
58
+ const slug = "slug" in params ? params.slug : params.evaluator;
59
+ const span = optionalCreateSpan({
60
+ trace: params.trace,
61
+ span: params.span,
62
+ name: params.name ? params.name : slug,
63
+ type: params.asGuardrail ? "guardrail" : "evaluation",
64
+ });
65
+
66
+ try {
67
+ const requestParams = prepareData({
68
+ ...params,
69
+ slug,
70
+ traceId: span?.trace.traceId,
71
+ spanId: span?.spanId,
72
+ span,
73
+ });
74
+
75
+ const response = await fetch(requestParams.url, {
76
+ method: "POST",
77
+ headers: requestParams.headers,
78
+ body: JSON.stringify(requestParams.json),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ throw new Error(`HTTP error! status: ${response.status}`);
83
+ }
84
+
85
+ const result = await response.json();
86
+ return handleResponse(result, span, params.asGuardrail);
87
+ } catch (e) {
88
+ return handleException(e as Error, span, params.asGuardrail);
89
+ }
90
+ };
91
+
92
+ const optionalCreateSpan = ({
93
+ trace,
94
+ span,
95
+ name,
96
+ type,
97
+ }: {
98
+ trace?: LangWatchTrace;
99
+ span?: LangWatchSpan;
100
+ name: string;
101
+ type: SpanTypes;
102
+ }): LangWatchSpan | undefined => {
103
+ if (span) {
104
+ return span.startSpan({ name, type });
105
+ } else if (trace) {
106
+ return trace.startSpan({ name, type });
107
+ }
108
+ return undefined;
109
+ };
110
+
111
+ const prepareData = (params: {
112
+ slug: string;
113
+ name?: string;
114
+ input?: string;
115
+ output?: string;
116
+ expectedOutput?: string;
117
+ contexts?: RAGChunk[] | string[];
118
+ conversation?: Conversation;
119
+ settings?: Record<string, unknown>;
120
+ traceId?: string;
121
+ spanId?: string;
122
+ span?: LangWatchSpan;
123
+ asGuardrail?: boolean;
124
+ }) => {
125
+ const data: Record<string, unknown> = {};
126
+ if (params.input) data.input = params.input;
127
+ if (params.output) data.output = params.output;
128
+ if (params.expectedOutput) data.expected_output = params.expectedOutput;
129
+ if (params.contexts && params.contexts.length > 0)
130
+ data.contexts = params.contexts;
131
+ if (params.conversation && params.conversation.length > 0)
132
+ data.conversation = params.conversation;
133
+
134
+ if (params.span) {
135
+ params.span.update({
136
+ input: { type: "json", value: data } as TypedValueJson,
137
+ params: params.settings,
138
+ });
139
+ }
140
+
141
+ return {
142
+ url: `${process.env.LANGWATCH_ENDPOINT}/api/evaluations/${params.slug}/evaluate`,
143
+ json: {
144
+ trace_id: params.traceId,
145
+ span_id: params.spanId,
146
+ name: params.name,
147
+ data,
148
+ settings: params.settings,
149
+ as_guardrail: params.asGuardrail,
150
+ },
151
+ headers: {
152
+ "X-Auth-Token": process.env.LANGWATCH_API_KEY ?? "",
153
+ "Content-Type": "application/json",
154
+ },
155
+ };
156
+ };
157
+
158
+ const handleResponse = (
159
+ response: EvaluationResultModel,
160
+ span?: LangWatchSpan,
161
+ asGuardrail = false
162
+ ): EvaluationResultModel => {
163
+ if (response.status === "error") {
164
+ response.details = response.details ?? "";
165
+ }
166
+
167
+ for (const key of Object.keys(response)) {
168
+ if (
169
+ response[key as keyof EvaluationResultModel] === null ||
170
+ response[key as keyof EvaluationResultModel] === undefined
171
+ ) {
172
+ delete response[key as keyof EvaluationResultModel];
173
+ }
174
+ }
175
+
176
+ if (span) {
177
+ const output: TypedValueGuardrailResult | TypedValueEvaluationResult =
178
+ asGuardrail
179
+ ? {
180
+ type: "guardrail_result",
181
+ value: response,
182
+ }
183
+ : {
184
+ type: "evaluation_result",
185
+ value: response,
186
+ };
187
+
188
+ span.update({ output });
189
+
190
+ if (response.cost) {
191
+ span.update({
192
+ metrics: {
193
+ cost: response.cost.amount,
194
+ },
195
+ });
196
+ }
197
+
198
+ span.end();
199
+ }
200
+
201
+ return response;
202
+ };
203
+
204
+ const handleException = (
205
+ e: Error,
206
+ span?: LangWatchSpan,
207
+ asGuardrail = false
208
+ ): EvaluationResultModel => {
209
+ const response: EvaluationResultModel = {
210
+ status: "error",
211
+ details: e.toString(),
212
+ };
213
+
214
+ if (asGuardrail) {
215
+ response.passed = true;
216
+ }
217
+
218
+ return handleResponse(response, span, asGuardrail);
219
+ };
package/src/index.test.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import { openai } from "@ai-sdk/openai";
12
12
  import { generateText, type CoreMessage } from "ai";
13
13
  import "dotenv/config";
14
+ import { version } from "../package.json";
14
15
 
15
16
  describe("LangWatch tracer", () => {
16
17
  let mockFetch: SpyInstanceFn;
@@ -65,6 +66,8 @@ describe("LangWatch tracer", () => {
65
66
  threadId: "123",
66
67
  userId: "456",
67
68
  labels: ["foo", "bar"],
69
+ sdkLanguage: "typescript",
70
+ sdkVersion: version,
68
71
  });
69
72
  expect(span.timestamps.startedAt).toBeDefined();
70
73
  expect(span.timestamps.finishedAt).toBeDefined();
@@ -133,6 +136,8 @@ describe("LangWatch tracer", () => {
133
136
  thread_id: "123",
134
137
  user_id: "456",
135
138
  labels: ["foo", "bar"],
139
+ sdk_language: "typescript",
140
+ sdk_version: version,
136
141
  });
137
142
  expect(requestBody.spans.length).toBe(3);
138
143
  });
package/src/index.ts CHANGED
@@ -2,11 +2,19 @@ import EventEmitter from "events";
2
2
  import { nanoid } from "nanoid";
3
3
  import { ZodError } from "zod";
4
4
  import { fromZodError } from "zod-validation-error";
5
- import { camelToSnakeCaseNested, type Strict } from "./helpers";
5
+ import { version } from "../package.json";
6
+ import {
7
+ evaluate,
8
+ type EvaluationParams,
9
+ type EvaluationResultModel,
10
+ } from "./evaluations";
11
+ import { LangWatchCallbackHandler } from "./langchain";
6
12
  import {
7
13
  type CollectorRESTParams,
14
+ type EvaluationResult,
8
15
  type Span as ServerSpan,
9
16
  type SpanTypes,
17
+ type TypedValueEvaluationResult,
10
18
  } from "./server/types/tracer";
11
19
  import {
12
20
  collectorRESTParamsSchema,
@@ -22,9 +30,15 @@ import {
22
30
  type PendingLLMSpan,
23
31
  type PendingRAGSpan,
24
32
  type RAGSpan,
33
+ type RESTEvaluation,
25
34
  type SpanInputOutput,
26
35
  } from "./types";
27
- import { captureError, convertFromVercelAIMessages } from "./utils";
36
+ import { camelToSnakeCaseNested, type Strict } from "./typeUtils";
37
+ import {
38
+ autoconvertTypedValues,
39
+ captureError,
40
+ convertFromVercelAIMessages,
41
+ } from "./utils";
28
42
 
29
43
  export type {
30
44
  BaseSpan,
@@ -39,7 +53,7 @@ export type {
39
53
  SpanInputOutput,
40
54
  };
41
55
 
42
- export { convertFromVercelAIMessages, captureError };
56
+ export { autoconvertTypedValues, captureError, convertFromVercelAIMessages };
43
57
 
44
58
  export class LangWatch extends EventEmitter {
45
59
  apiKey: string | undefined;
@@ -132,12 +146,35 @@ export class LangWatch extends EventEmitter {
132
146
  }
133
147
  }
134
148
 
149
+ type CurrentSpan = {
150
+ current: LangWatchSpan;
151
+ previous?: CurrentSpan;
152
+ };
153
+
154
+ type AddEvaluationParams = {
155
+ evaluationId?: string;
156
+ span?: LangWatchSpan;
157
+ name: string;
158
+ type?: string;
159
+ isGuardrail?: boolean;
160
+ status?: "processed" | "skipped" | "error";
161
+ passed?: boolean;
162
+ score?: number;
163
+ label?: string;
164
+ details?: string;
165
+ error?: Error;
166
+ timestamps?: RESTEvaluation["timestamps"];
167
+ };
168
+
135
169
  export class LangWatchTrace {
136
170
  client: LangWatch;
137
171
  traceId: string;
138
172
  metadata?: Metadata;
139
173
  finishedSpans: Record<string, ServerSpan> = {};
140
- timeoutRef?: NodeJS.Timeout;
174
+ langchainCallback?: LangWatchCallbackHandler;
175
+ evaluations: RESTEvaluation[] = [];
176
+ private currentSpan?: CurrentSpan;
177
+ private timeoutRef?: NodeJS.Timeout;
141
178
 
142
179
  constructor({
143
180
  client,
@@ -150,7 +187,11 @@ export class LangWatchTrace {
150
187
  }) {
151
188
  this.client = client;
152
189
  this.traceId = traceId;
153
- this.metadata = metadata;
190
+ this.metadata = {
191
+ ...metadata,
192
+ sdkVersion: version,
193
+ sdkLanguage: "typescript",
194
+ };
154
195
  }
155
196
 
156
197
  update({ metadata }: { metadata: Metadata }) {
@@ -158,16 +199,37 @@ export class LangWatchTrace {
158
199
  ...this.metadata,
159
200
  ...metadata,
160
201
  ...(typeof metadata.labels !== "undefined"
161
- ? { labels: [...(this.metadata?.labels ?? []), ...metadata.labels] }
202
+ ? {
203
+ labels: [
204
+ ...(this.metadata?.labels ?? []),
205
+ ...(metadata.labels ?? []),
206
+ ],
207
+ }
162
208
  : {}),
163
209
  };
164
210
  }
165
211
 
212
+ setCurrentSpan(span: LangWatchSpan) {
213
+ this.currentSpan = {
214
+ current: span,
215
+ previous: this.currentSpan,
216
+ };
217
+ }
218
+
219
+ getCurrentSpan() {
220
+ return this.currentSpan?.current;
221
+ }
222
+
223
+ resetCurrentSpan() {
224
+ this.currentSpan = this.currentSpan?.previous;
225
+ }
226
+
166
227
  startSpan(params: Omit<Partial<PendingBaseSpan>, "parentId">) {
167
228
  const span = new LangWatchSpan({
168
229
  trace: this,
169
230
  ...params,
170
231
  });
232
+ this.setCurrentSpan(span);
171
233
  return span;
172
234
  }
173
235
 
@@ -176,6 +238,7 @@ export class LangWatchTrace {
176
238
  trace: this,
177
239
  ...params,
178
240
  });
241
+ this.setCurrentSpan(span);
179
242
  return span;
180
243
  }
181
244
 
@@ -184,11 +247,113 @@ export class LangWatchTrace {
184
247
  trace: this,
185
248
  ...params,
186
249
  });
250
+ this.setCurrentSpan(span);
187
251
  return span;
188
252
  }
189
253
 
254
+ addEvaluation = ({
255
+ evaluationId,
256
+ span,
257
+ name,
258
+ type,
259
+ isGuardrail,
260
+ status = "processed",
261
+ passed,
262
+ score,
263
+ label,
264
+ details,
265
+ error,
266
+ timestamps,
267
+ }: AddEvaluationParams): void => {
268
+ const currentEvaluationIndex = this.evaluations.findIndex(
269
+ (e) =>
270
+ evaluationId && "evaluationId" in e && e.evaluationId === evaluationId
271
+ );
272
+
273
+ const currentEvaluation =
274
+ currentEvaluationIndex !== -1
275
+ ? this.evaluations[currentEvaluationIndex]
276
+ : undefined;
277
+
278
+ const evaluationResult: EvaluationResult = {
279
+ status,
280
+ ...(passed !== undefined && { passed }),
281
+ ...(score !== undefined && { score }),
282
+ ...(label !== undefined && { label }),
283
+ ...(details !== undefined && { details }),
284
+ };
285
+
286
+ let span_ = span;
287
+ if (!span_) {
288
+ span_ = this.startSpan({
289
+ type: "evaluation",
290
+ });
291
+ }
292
+ if (span_.type !== "evaluation") {
293
+ span_ = span_.startSpan({ type: "evaluation" });
294
+ }
295
+
296
+ span_.update({
297
+ name,
298
+ output: {
299
+ type: "evaluation_result",
300
+ value: evaluationResult,
301
+ } as TypedValueEvaluationResult,
302
+ error,
303
+ timestamps: timestamps
304
+ ? {
305
+ startedAt: timestamps.startedAt ?? span_.timestamps.startedAt,
306
+ finishedAt: timestamps.finishedAt ?? undefined,
307
+ }
308
+ : undefined,
309
+ });
310
+ span_.end();
311
+
312
+ const evaluation: RESTEvaluation = {
313
+ evaluationId: evaluationId ?? `eval_${nanoid()}`,
314
+ spanId: span_.spanId,
315
+ name,
316
+ type,
317
+ isGuardrail,
318
+ status,
319
+ passed,
320
+ score,
321
+ label,
322
+ details,
323
+ error: error ? captureError(error) : undefined,
324
+ timestamps: timestamps ?? {
325
+ startedAt: span_.timestamps.startedAt,
326
+ finishedAt: span_.timestamps.finishedAt,
327
+ },
328
+ };
329
+
330
+ if (currentEvaluation && currentEvaluationIndex !== -1) {
331
+ this.evaluations[currentEvaluationIndex] = {
332
+ ...currentEvaluation,
333
+ ...evaluation,
334
+ };
335
+ } else {
336
+ this.evaluations.push(evaluation);
337
+ }
338
+ };
339
+
340
+ async evaluate(params: EvaluationParams): Promise<EvaluationResultModel> {
341
+ return evaluate({
342
+ trace: this,
343
+ ...params,
344
+ });
345
+ }
346
+
347
+ getLangChainCallback() {
348
+ if (!this.langchainCallback) {
349
+ this.langchainCallback = new LangWatchCallbackHandler({ trace: this });
350
+ }
351
+ return this.langchainCallback;
352
+ }
353
+
190
354
  onEnd(span: ServerSpan) {
191
355
  this.finishedSpans[span.span_id] = span;
356
+ this.resetCurrentSpan();
192
357
  this.delayedSendSpans();
193
358
  }
194
359
 
@@ -206,8 +371,9 @@ export class LangWatchTrace {
206
371
  try {
207
372
  trace = collectorRESTParamsSchema.parse({
208
373
  trace_id: this.traceId,
209
- metadata: camelToSnakeCaseNested(this.metadata),
374
+ metadata: camelToSnakeCaseNested(this.metadata, "metadata"),
210
375
  spans: Object.values(this.finishedSpans),
376
+ evaluations: camelToSnakeCaseNested(this.evaluations),
211
377
  } as Strict<CollectorRESTParams>);
212
378
  } catch (error) {
213
379
  if (error instanceof ZodError) {
@@ -300,6 +466,7 @@ export class LangWatchSpan implements PendingBaseSpan {
300
466
  parentId: this.spanId,
301
467
  ...params,
302
468
  });
469
+ this.trace.setCurrentSpan(span);
303
470
  return span;
304
471
  }
305
472
 
@@ -309,6 +476,7 @@ export class LangWatchSpan implements PendingBaseSpan {
309
476
  parentId: this.spanId,
310
477
  ...params,
311
478
  });
479
+ this.trace.setCurrentSpan(span);
312
480
  return span;
313
481
  }
314
482
 
@@ -318,9 +486,24 @@ export class LangWatchSpan implements PendingBaseSpan {
318
486
  parentId: this.spanId,
319
487
  ...params,
320
488
  });
489
+ this.trace.setCurrentSpan(span);
321
490
  return span;
322
491
  }
323
492
 
493
+ addEvaluation(params: AddEvaluationParams) {
494
+ this.trace.addEvaluation({
495
+ ...params,
496
+ span: this,
497
+ });
498
+ }
499
+
500
+ async evaluate(params: EvaluationParams): Promise<EvaluationResultModel> {
501
+ return evaluate({
502
+ span: this,
503
+ ...params,
504
+ });
505
+ }
506
+
324
507
  end(params?: Partial<Omit<PendingBaseSpan, "spanId" | "parentId">>) {
325
508
  this.timestamps.finishedAt = Date.now();
326
509
  if (params) {